Git Product home page Git Product logo

jhack's Introduction

jhack - Make charming charming again!

jhack foo Awesome

This is a homegrown collection of opinionated scripts and utilities to make the charm dev's life somewhat easier.

This README is meant as overview of what's there, and high-level documentation of the commands. More extensive documentation is provided in the cli itself. All commands can be called with --help and will provide more information than what is included here.

Installation:

from sources (dev setup):

Clone the repo

pip install -r requirements.txt
pip install -e .
as package (requires setuptools 61 or later):
pip install git+https://github.com/PietroPasotti/jhack
as snap:
sudo snap install --edge jhack
sudo snap connect jhack:dot-local-share-juju snapd
sudo snap connect jhack:ssh-read snapd

The last binding is only necessary if you work on LXD models and use commands that rely on juju ssh/juju scp.

Usage:

jhack [category] [command]

for example:

jhack utils tail
jhack model rm

Building:

pip install build

python -m build

Happy hacking!

Disclaimer

Many of the commands jhack offers are pretty destructive or bring a high risk of catastrophic failure and random explosions.

These commands will prompt the user for confirmation before proceeding, after, whenever possible, showing the low-level (Juju) commands it would run.

Enabling devmode

In devmode, destructive commands will be run without requiring a confirmation prompt.

You can permanently enable devmode by setting ~/.config/jhack/config.toml (see jhack conf) and set [general]enable_destructive_commands_NO_PRODUCTION_zero_guarantees to true.

Otherwise, set the JHACK_PROFILE=devmode envvar to run a single command without the confirmation prompt.

As only exception, nuke has a double safeguard in that even if you enable devmode, you will still get a confirmation prompt. To disable that one, set [nuke]ask_for_confirmation to false.

utils

sync

jhack utils sync application-name/0 -s ./src -s ./lib

Will watch the ./src and ./lib folders (recursively, by default) for changes and push any to the application-name/0 unit.

Pro tip: jhack utils sync application-name -s ./src will sync to all units of application-name!

Pro-pro tip: jhack sync * -s ./lib will sync to all applications in the current model! Handy when you're working on shared (relation interface, for example) libraries.

unbork-juju

jhack utils unbork-juju

Does exactly what it says, and it does it pretty well.

ffwd

jhack utils ffwd

Fast-forwards the firing of update-status hooks, and restores it to a 'slow' firing rate after the process is killed or after a given timeout.

Self-explanation:

jhack utils ffwd
  --timeout 10 # exits after 10 seconds
  --fast-interval 5 # update-status fires each 5 seconds
  --slow-interval 50m # when done, set update-status firing rate to 50 minutes.

tail

Monitors the logs and gathers all logs concerning events being fired on the units. Will pprint the last N in a nice format. Keeps listening and updates in the background as new units are added.

┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ timestamp ┃ traefik-k8s/0                ┃ prometheus-k8s/1             ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ 13:37:15  │                              │ ingress-relation-changed     │
│ 13:37:14  │                              │ ingress-relation-joined      │
│ 13:37:14  │                              │ ingress-relation-changed     │
│ 13:37:13  │                              │ prometheus-peers-relation-c… │
│ 13:37:12  │                              │ prometheus-peers-relation-j… │
│ 13:37:12  │                              │ prometheus-pebble-ready      │
│ 13:37:11  │                              │ start                        │
│ 13:37:10  │                              │ config-changed               │
│ 13:37:09  │                              │                              │
│ 13:37:09  │                              │ database-storage-attached    │
│ 13:37:09  │ ingress-per-unit-relation-c… │                              │
│ 13:37:08  │                              │ leader-settings-changed      │
│ 13:37:08  │ ingress-per-unit-relation-c… │                              │
│ 13:37:08  │                              │                              │
│ 13:37:08  │                              │ ingress-relation-created     │
│ 13:37:07  │ ingress-per-unit-relation-j… │                              │
│ 13:37:07  │                              │                              │
│ 13:37:07  │                              │ prometheus-peers-relation-c… │
│ 13:37:06  │                              │ install                      │
└───────────┴──────────────────────────────┴──────────────────────────────┘

There's more!

You can use tail to visualize deferrals in ops.

If you pass the -d flag, short for --show-defer, whenever an event is deferred, reemitted, or re-deferred, you'll be able to follow it right along the tail. You might see then something like:

┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ timestamp                ┃ trfk/0                                ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ 14:02:53                 │                                     │ │
│ 14:01:36                 │ event_3                           ❯─┘ │
│ 13:56:49                 │ ingress_per_unit_relation_changed     │
│ 13:56:47                 │ ingress_per_unit_relation_changed     │
│ 13:56:47                 │ ingress_per_unit_relation_changed     │
│ 13:56:46                 │ ingress_per_unit_relation_joined      │
│ 13:56:46                 │ event_3                           ❮─┐ │
│ 13:56:46                 │ ingress_per_unit_relation_created   │ │
│ 13:46:30                 │ event_3                            ⭘┤ │
│ 13:41:51                 │ event_3                           ❯─┘ │
│ 13:41:51                 │ event_2                           ❮─┐ │
│ 13:36:50                 │ event_2                           ❯─┘ │
│ 13:36:50                 │ event_1                           ❮─┐ │
│ 13:31:29                 │ event_1                           ❯─┘ │

                            [...]

The little circle is event-3 getting re-emitted and immediately re-deferred!

The graph can get nice and messy if multiple events get deferred in an interleaved fashion, enabling you to see what's going on. Which is nice.

update_status ❮──────┐
update_status   .....│
update_status  ⭘─────┤
update_status   .....│
update_status  ⭘─────┤
update_status ❮─────┐│
update_status ❯─────┼┘
update_status  ⭘────┤
update_status ❮────┐│
update_status ❯────┼┘
update_status ❮────┼┐
update_status  ⭘───┤│
update_status ❯────┼┘
update_status  ⭘───┤
update_status ❮───┐│
update_status ❮──┐││
update_status ❯──┼┼┘
update_status  ⭘─┼┤
update_status  ⭘─┤│
update_status ❯──┼┘

And did I mention that there's colors?

You can also tail saved logs

Say you have saved two debug-logs with:

juju debug-log --date -i prom/0 > prom.log
juju debug-log --date -i trfk/0 > trfk.log

Yielding files:

prom.txt

unit-prom-0: 2022-07-20 10:00:00 INFO juju.worker.uniter.operation ran "install" hook (via hook dispatching script: dispatch)
unit-prom-0: 2022-07-21 5:00:00 INFO juju.worker.uniter.operation ran "prometheus-peers-relation-created" hook (via hook dispatching script: dispatch)

trfk.txt

unit-trfk-0: 2022-07-20 11:00:00 INFO juju.worker.uniter.operation ran "start" hook (via hook dispatching script: dispatch)
unit-trfk-0: 2022-07-20 12:00:00 INFO juju.worker.uniter.operation ran "traefik-pebble-ready" hook (via hook dispatching script: dispatch)

You can run jhack utils tail --file=prom.txt --file=trfk.txt to see the events in all the logs, interlaced in the correct chronological order as expected:

┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ timestamp      ┃ prom/0                               ┃ trfk/0                  ┃
┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━┩
│  5:00:00       │ prometheus_peers_relation_created    │                         │
│ 12:00:00       │                                      │ traefik_pebble_ready    │
│ 11:00:00       │                                      │ start                   │
│ 10:00:00       │ install                              │                         │
├────────────────┼──────────────────────────────────────┼─────────────────────────┤
│ The end.       │ prom/0                               │ trfk/0                  │
├────────────────┼──────────────────────────────────────┼─────────────────────────┤
│ events emitted │ 2                                    │ 2                       │
└────────────────┴──────────────────────────────────────┴─────────────────────────┘

show-relation

Displays the databags of units involved in a relation. if the endpoint is of the form app-name/id:relation-name: it will display the application databag and the one for the unit with id=id. If the endpoint is of the form app-name:relation-name: it will display the application databag and the databags for all units. Examples:

jhack utils show-relation ipu:ingress-per-unit traefik-k8s:ingress-per-unit

jhack utils show-relation ipu/0:ingress-per-unit traefik-k8s:ingress-per-unit

jhack utils show-relation ipu/0:ingress-per-unit traefik-k8s/2:ingress-per-unit

Example output:

                                                      relation data v0.2
┏━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ category         ┃ traefik-k8s                                         ┃ ipu                                                ┃
┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ relation name    │ ingress-per-unit                                    │ ingress-per-unit                                   │
│ interface        │ ingress_per_unit                                    │ ingress_per_unit                                   │
│ leader unit      │ 0                                                   │ 0                                                  │
├──────────────────┼─────────────────────────────────────────────────────┼────────────────────────────────────────────────────┤
│ application data │ ╭─────────────────────────────────────────────────╮ │ ╭────────────────────────────────────────────────╮ │
│                  │ │                                                 │ │ │ <empty>                                        │ │
│                  │ │  ingress  ipu/0:                                │ │ ╰────────────────────────────────────────────────╯ │
│                  │ │             url:                                │ │                                                    │
│                  │ │           http://my.it:80/test-charm-ipu-9dg8…  │ │                                                    │
│                  │ │           ipu/1:                                │ │                                                    │
│                  │ │             url:                                │ │                                                    │
│                  │ │           http://my.it:80/test-charm-ipu-9dg8…  │ │                                                    │
│                  │ │           ipu/2:                                │ │                                                    │
│                  │ │             url:                                │ │                                                    │
│                  │ │           http://my.it:80/test-charm-ipu-9dg8…  │ │                                                    │
│                  │ ╰─────────────────────────────────────────────────╯ │                                                    │
│ unit data        │ ╭─ traefik-k8s/0* ─╮ ╭─ traefik-k8s/1 ─╮            │ ╭─ ipu/0*  ────────────────────╮                   │
│                  │ │ <empty>          │ │ <empty>         │            │ │                              │                   │
│                  │ ╰──────────────────╯ ╰─────────────────╯            │ │  host   foo.bar              │                   │
│                  │ ╭─ traefik-k8s/2 ──╮ ╭─ traefik-k8s/3 ─╮            │ │  model  test-charm-ipu-9dg8  │                   │
│                  │ │ <empty>          │ │ <empty>         │            │ │  name   ipu/0                │                   │
│                  │ ╰──────────────────╯ ╰─────────────────╯            │ │  port   80                   │                   │
│                  │                                                     │ ╰──────────────────────────────╯                   │
│                  │                                                     │ ╭─ ipu/1  ─────────────────────╮                   │
│                  │                                                     │ │                              │                   │
│                  │                                                     │ │  host   foo.bar              │                   │
│                  │                                                     │ │  model  test-charm-ipu-9dg8  │                   │
│                  │                                                     │ │  name   ipu/1                │                   │
│                  │                                                     │ │  port   80                   │                   │
│                  │                                                     │ ╰──────────────────────────────╯                   │
│                  │                                                     │ ╭─ ipu/2  ─────────────────────╮                   │
│                  │                                                     │ │                              │                   │
│                  │                                                     │ │  host   foo.bar              │                   │
│                  │                                                     │ │  model  test-charm-ipu-9dg8  │                   │
│                  │                                                     │ │  name   ipu/2                │                   │
│                  │                                                     │ │  port   80                   │                   │
│                  │                                                     │ ╰──────────────────────────────╯                   │
└──────────────────┴─────────────────────────────────────────────────────┴────────────────────────────────────────────────────┘

Since v0.3, also peer relations are supported. Additionally, it supports “show me the nth relation” instead of having to type out the whole app-name:endpoint thing: if you have 3 relations in your model, you can simply do jhack show-relation -n 1 and jhack will print out the 2nd relation from the top (of the same list appearing when you do juju status --relations, that is.

show-stored

As we know, ops offers the possibility to use StoredState to persist some data between events, making charms therefore (somewhat) stateful. It can be challenging (or simply tedious) during testing and debugging, to inspect the contents of a live charm’s stored state in a uniform way.

Well, no more!

Suppose you have a prometheus-k8s charm deployed as prom (and related to traefik-k8s). Type: jhack show-stored prom/0 and you'd get:

                                                      stored data v0.1
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ PrometheusCharm.GrafanaDashboardProvider._stored            ┃ PrometheusCharm.ingress._stored                            ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│                                                             │                                                            │
│  handle:  PrometheusCharm/GrafanaDashboardProvider[grafan…  │  handle:  PrometheusCharm/IngressPerUnitRequirer[ingress…  │
│    size:  8509b                                             │    size:  657b                                             │
│                           <dict>                            │                           <dict>                           │
│ ┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ │ ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ │
│ ┃ key                   ┃ value                           ┃ │ ┃ key            ┃ value                                 ┃ │
│ ┡━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ ┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │
│ │ 'dashboard_templates' │ {'file:prometheus-k8s_rev1.jso… │ │ │ 'current_urls' │ {'prom/0':                            │ │
│ │                       │ {'charm': 'prometheus-k8s',     │ │ │                │ 'http://0.0.0.0:80/baz-prom-0'}       │ │
│ │                       │ 'content':                      │ │ └────────────────┴───────────────────────────────────────┘ │
│ │                       │ '/Td6WFoAAATm1rRGAgAhARYAAAB0L… │ │                                                            │
│ │                       │ 'juju_topology': {'model':      │ │                                                            $
│ │                       │ 'baz', 'model_uuid':            │ │                                                            │
│ │                       │ '00ff58ab-c187-497d-85b3-7cadd… │ │                                                            │
│ │                       │ 'application': 'prom', 'unit':  │ │                                                            │
│ │                       │ 'prom/0'}}}                     │ │                                                            │
│ └───────────────────────┴─────────────────────────────────┘ │                                                            │
└─────────────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────┘

Adapters

The bottom part of the two table cells contains the ‘blob’ itself. At the moment we only implement ‘pretty-printers’ for python dicts. ops natively serializes only native python datatypes (anything you can `yaml.dump, in fact), but you could be serializing much more complex stuff than that.

For that reason, jhack show-stored exposes an --adapters optional argument, which allows you to inject your custom adapter to deserialize a specific handle. So, for example, if you are not happy with how the ingress StoredData is represented, you could create a file:

from urllib.parse import urlparse
from rich.table import Table
def _deserialize_ingress(raw: dict):
    urls = raw['current_urls']
    table = Table(title='ingress view adapter')
    table.add_column('unit')
    table.add_column('scheme')
    table.add_column('hostname')
    table.add_column('port')
    table.add_column('path')

    for unit_name, url in urls.items():
        row = [unit_name]

        p_url = urlparse(url)
        hostname, port = p_url.netloc.split(":")
        row.extend((p_url.scheme, hostname, port, p_url.path))

        table.add_row(*row)

    return table  # we can return any rich.RenderableType (str, or Rich builtins)

# For this to work, this file needs to declare a global 'adapters' var of the right type.
adapters = {
    "PrometheusCharm/IngressPerUnitRequirer[ingress]/StoredStateData[_stored]": _deserialize_ingress
}

And then by running jhack show-stored -a /path/to/that/file, you’d magically get:

                                                      stored data v0.1
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ PrometheusCharm.GrafanaDashboardProvider._stored            ┃ PrometheusCharm.ingress._stored                            ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│                                                             │                                                            │
│  handle:  PrometheusCharm/GrafanaDashboardProvider[grafan…  │  handle:  PrometheusCharm/IngressPerUnitRequirer[ingress…  │
│    size:  8509b                                             │    size:  657b                                             │
│                           <dict>                            │                ingress view adapter                        │
│ ┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ │ ┏━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━┓        │
│ ┃ key                   ┃ value                           ┃ │ ┃ unit   ┃ scheme ┃ hostname ┃ port ┃ path        ┃        │
│ ┡━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ ┡━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━┩        │
│ │ 'dashboard_templates' │ {'file:prometheus-k8s_rev1.jso… │ │ │ prom/0 │ http   │ 0.0.0.0  │ 80   │ /baz-prom-0 │        │
│ │                       │ {'charm': 'prometheus-k8s',     │ │ └────────┴────────┴──────────┴──────┴─────────────┘        │
│ │                       │ 'content':                      │ │                                                            │
│ │                       │ '/Td6WFoAAATm1rRGAgAhARYAAAB0L… │ │                                                            │
│ │                       │ 'juju_topology': {'model':      │ │                                                            │
│ │                       │ 'baz', 'model_uuid':            │ │                                                            │
│ │                       │ '00ff58ab-c187-497d-85b3-7cadd… │ │                                                            │
│ │                       │ 'application': 'prom', 'unit':  │ │                                                            │
│ │                       │ 'prom/0'}}}                     │ │                                                            │
│ └───────────────────────┴─────────────────────────────────┘ │                                                            │
└─────────────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────┘

Which is pretty cool.

nuke

This utility is the swiss army knife of "just get rid of this thing already". The broad goal is to have one easy-to-use command to destroy things in the most dirty and unsafe way possible, just please make it fast and please don't make me type all those letters out.

The tool is designed to be used with juju status --relations and juju models.

The basic usage is as follows:

jhack nuke -> will nuke the current model and that's that. jhack nuke foo* -> will:

  • scan juju models for models whose name begins with "foo" and nuke each one of them.
  • For each model it did not target as nukeable in the previous step, it will scan juju status -m that-model and:
    • for each app whose name begins with "foo", it will nuke it.
    • for each relation NOT involving an app selected for nukage in the previous step, if either the provider or requirer starts with "foo", it will nuke it.

You can switch between "starts with" / "ends with" and "contains" matching modes by placing stars around the string:

  • jhack nuke foo --> same as jhack nuke foo*
  • jhack nuke *foo --> same algorithm as above, but will nuke stuff whose name ends with "foo".
  • jhack nuke *foo* --> ... will nuke stuff that contains "foo"
  • jhack nuke !foo --> exact match only. So it will presumably only nuke one thing, except if you have many models with identically-named apps or relations in them. Then they'll all be vanquished.

For targeting relations only, you can type out the endpoint name up to and including the colon. For example, for purging all relations involving your my-db application, you could do: jhack nuke "my-db:", that will match all the relations of your app. They're history now.

By using this tool you acknowledge the possibility of it bricking your model or controller. Hopefully nothing beyond that.

Safety tips

  • Learn to use the command by trying out the --dry-run flag first, that will print out what it would nuke without actually nuking anything.

  • The command has an optional -n flag that allows you to specify the expected number of nukes that should be fired out. If more or less than n nukeables are matched, the command will print an error message and abort.

  • The command has a --selectors (-s) option that can be used to specify what to include/exclude in the bombardment.

    • 'a' for apps, 'A' for all except apps
    • 'm' for models, 'M' for all except models
    • 'r' for relations, 'R' for all except relations (although, the resulting nuke will probably also wipe the relations that would have been matched had this flag been omitted)

    So, for example, jhack nuke -s M foo will nuke all apps and relations it can find matching 'foo', equivalent to jhack nuke -s ar foo.

YOLO mode

Even in devmode, nuke has a confirmation prompt. Look at jhack conf to find out how you can set [nuke]ask_for_confirmation = false.

fire

This command is used to simulate a specific event on a live unit. It works by building up an environment from scratch and tricking the charm to think a specific event is running by using juju exec. You can use it to simulate update-status, and other 'simple' events that have no special requirements in terms of envvars being set, but also more complex events are supported (relation events, workload events).

Examples:

jhack fire traefik/0 update-status

jhack fire traefik/leader traefik-pebble-ready

jhack fire traefik/0 ingress-per-unit-relation-changed

Pro tip: use fire it in combination with jhack sync to quickly iterate and repeatedly execute the charm 'as if' a specific event was being fired.

Caveats: careless usage can rapidly and irrevocably bork your charm, since the events being fired in this way are not real juju events, therefore the charm state and the juju state can rapidly desync. E.g. Juju thinks everything is fine with the charm, but you just simulated a 'remove' event, following which the charm duly stopped all workload services, cleared a database and got ready to gracefully teardown. If after that Juju decides to fire any event, the charm will rightfully be surprised because it thought it was about to be killed.

However, if all of your event handlers truly are idempotent (hem hem) you should be fine.

Note: you can do

jhack fire traefik update-status

to fire the event on all traefik units!

replay

This command offers facilities to capture runtime event contexts and use them to 're-fire' "the same event" later on. Unlike jhack utils fire, which uses a synthetic (i.e. built from scratch, minimal) environment, this command family gets a hold of a 'real' environment, serializes it, and reuses it as needed. So the env is more complete, and the simulation, more realistic. The flow consists of two main steps:

  • inject code that captures any event, serializes it and dumps it to a db on the unit.
  • whenever you like, trigger a charm execution reusing a recorded context.

install

This command is used to inject into a unit the code responsible for capturing the context in which the charm runs and dropping it to a db file.

Example usage:

jhack replay install trfk/0

list

This command is used to enumerate, first to last, all events which have been fired onto a unit (since replay was installed!). You can use the index of the enumeration to later re-fire the event.

Example:

jhack replay list trfk/0

its output could be something like:

Listing recorded events:
    (0) 2022-09-12 11:54:02.279174 :: start
    (1) 2022-09-12 11:54:02.768836 :: ingress-per-unit-relation-created
    (2) 2022-09-12 11:54:03.293178 :: ingress-per-unit-relation-joined
    (3) 2022-09-12 11:54:03.810452 :: ingress-per-unit-relation-changed
    (4) 2022-09-12 11:54:04.369351 :: ingress-per-unit-relation-joined
    (5) 2022-09-12 11:54:04.924288 :: ingress-per-unit-relation-changed
    (6) 2022-09-12 11:54:10.371510 :: traefik-pebble-ready

or if no events have been fired yet:

Listing recorded events:
    <no events>

Tip: to quickly get some events in, you could jhack fire trfr/0 update-status.

emit

This command is used to re-fire a recorded event onto the same unit.

jhack replay emit trfk/0 2

Note that the index needs to match that of some recorded event (you can inspect those with jhack replay list).

Example run:

$ jhack replay install trfk/0
$ jhack replay list trfk/0
Listing recorded events:
    (0) 2022-09-12 11:54:02.279174 :: start
    (1) 2022-09-12 11:54:02.768836 :: ingress-per-unit-relation-created
    (2) 2022-09-12 11:54:03.293178 :: ingress-per-unit-relation-joined
    (3) 2022-09-12 11:54:03.810452 :: ingress-per-unit-relation-changed
$ jhack replay emit trfk/0 2
Replaying event (3): ingress-per-unit-relation-joined as originally emitted at 2022-09-12 11:54:03.293178.

dump

Dump a recorded event (raw json). Interesting if you want to inspect the event context, or if you want to re-use it in other scripts (e.g. with jhack utils fire).

Runtime

In jhack.utils.event_recorder.runtime you can find a Runtime class. That object can be used, in combination with the json database you obtained via replay dump, to locally execute a charm by simulating the context "exactly" as it occurred during the recorded execution.

At some point it will be moved to a charm lib.

The main use cases for Runtime are:

  • regression testing:
    • install replay on some unit,
    • wait for some event to bork your charm
    • grab the event db and put it in some /tests/replay_data folder
    • use Runtime to mock a charm execution using that backing database so that the charm will run again "exactly as it did back then"
    • Assert that the charm does not bork exactly as it did back then
  • local debugging:
    • use your favourite ide debugger tool to step through charm code without having to do any mocking at all: all juju-facing calls will return 'as they did in real life'.

Future work:

  • make it easier to manually edit the contents of the event database, to turn Runtime into a Scenario mocking lib. What if instead of returning True, leader-get returned False at that point?
  • Reuse the @memo injection facilities to reroute locally originating juju/pebble client calls to a specific remote controller and pebble server. Goal: be able to run a charm ANYWHERE but have it talk to a real backend living somewhere else.

model

clear

jhack model clear

Will nuke all applications in the current model.

rm

jhack model rm

Will nuke the current model.

charm

update

Updates a packed .charm file by dumping into it any number of directories.

jhack charm update ./my_charm_file-amd64.charm --src ./src --dst src

This will take ./src and recursively copy it into the packed charm's /src dir (it will destroy any existing content).

sync

Like update, but keeps watching for changes in the provided directories and pushes them into the packed charm whenever there's one.

jhack charm sync ./my_charm_file-amd64.charm --src ./src --dst src

provision

Run a script in one or multiple units.

When debugging, it's often handy to install certain tools on a running unit, to then shell into it and start hacking around. How often have you:

juju ssh foo/0
apt update
apt install vim procps top mc -y

Well, no more!

The idea is: you create your unit provisioning script in ~/.cprov/default, keeping in mind that no user input can be expected (i.e. put -y flags everywhere).

Running

jhack charm provision traefik-k8s/1;prometheus-k8s

will run that script on all prometheus units, and traefik's unit 1. You can put multiple scripts in ~/.cprov, and choose which one to use by:

jhack charm provision foo/1 --script foo

Alternatively, you can pass a full path, and it will not matter where the file is:

jhack charm provision foo/1 --script /path/to/your/script.sh

repack

Used to pack a charm and refresh it in a juju model. Useful when developing. If used without arguments, it will assume the cwd is the charm's root, will run charmcraft pack, and grab the application name from the charm's name.

jhack charm repack

Otherwise, you can specify a folder where the packing should be done, and an application name to target with the refresh.

jhack charm repack --root /where/my/charm/root/is --name juju-app-name

vinfo

vinfo is a command to show in tabular format the full version fingerprint of a charmed unit or application.

jhack vinfo my-app will show (for example):

                               vinfo v0.1
┌─────────────────────────────────────────────┬────────────────────────┐
│ app name                                    │ my-app/0               │
│ charm                                       │ my-app: v33 - stable   │
│ model                                       │ foo                    │
│ workload version                            │ 0.3.42                 │
├─────────────────────────────────────────────┼────────────────────────┤
│ grafana_k8s:grafana_dashboard               │ 0.13                   │
│ prometheus_k8s:prometheus_scrape            │ 0.21                   │
│ loki_k8s:loki_push_api                      │ 0.12                   │
│ parca:parca_scrape                          │ 0.2                    │
│ traefik_k8s:ingress                         │ 1.3                    │
│ observability_libs:kubernetes_service_patch │ 0.6                    │
│ observability_libs:juju_topology            │ 0.2                    │
└─────────────────────────────────────────────┴────────────────────────┘

To also check the charm lib versions against the latest available upstream: jhack charm vinfo -o my-app

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ app name                                    ┃ my-app/0               ┃
│ charm                                       │ my-app: v33 - stable   │
│ model                                       │ foo                    │
│ workload version                            │ 0.3.42                 │
├─────────────────────────────────────────────┼────────────────────────┤
│ grafana_k8s:grafana_dashboard               │ 0.13    < (0.17)       │
│ prometheus_k8s:prometheus_scrape            │ 0.21    < (0.25)       │
│ loki_k8s:loki_push_api                      │ 0.12    ==             │
│ parca:parca_scrape                          │ 0.2     < (0.3)        │
│ traefik_k8s:ingress                         │ 1.3     < (1.5)        │
│ observability_libs:kubernetes_service_patch │ 0.6     < (1.5)        │
│ observability_libs:juju_topology            │ 0.2     < (0.4)        │
└─────────────────────────────────────────────┴────────────────────────┘

list-endpoints

The list-endpoints command is a utility to view the integration endpoints a charm has to offer.

                                                           endpoints v0.1                                                           
┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ role          ┃ endpoint                   ┃ interface                       ┃ version       ┃ bound to                          ┃
┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ requires      │ certificates               │ tls-certificates                │ 2.10          │ -                                 │
│               │ logging                    │ loki_push_api                   │ 0.21          │ -                                 │
│               │ tracing                    │ tracing                         │ 0.6           │ -                                 │
│               │ receive-ca-cert            │ certificate_transfer            │ 0.4           │ -                                 │
│ provides      │ ingress                    │ ingress                         │ 1.17|2.5      │ alertmanager, catalogue           │
│               │ ingress-per-unit           │ ingress_per_unit                │ 1.15          │ loki, prometheus                  │
│               │ metrics-endpoint           │ prometheus_scrape               │ 0.41          │ prometheus                        │
│               │ traefik-route              │ traefik_route                   │ 0.7           │ grafana                           │
│               │ grafana-dashboard          │ grafana_dashboard               │ 0.34          │ -                                 │
│ peers         │ peers                      │ traefik_peers                   │ n/a           │ <itself>                          │
└───────────────┴────────────────────────────┴─────────────────────────────────┴───────────────┴───────────────────────────────────┘

in order to show the version of the library implementing a given interface, jhack naively attempts to match the interface name to a library file. It is standard practice to put a library for the foo-bar-baz interface in lib/charms/owner_charm/v42/foo_bar_baz.py, which allows jhack to deduce that the version of the library is 42.[whatever is in LIBPATCH]. This however is not enforced; in that case you'll see a <library not found> tag instead.

If multiple library files are found for a given interface, such as in the case of ingress in the example output above, jhack will assume that the charm supports both versions and print them all.

pull-cmr

Ever went to the trouble of juju offer, then juju consume, juju relate? Done it once, and never ever want to do it again because you keep forgetting the commands, the arguments, the syntax? Well, search no more. jhack pull-cmr some-model. You'll see something like:

 ~
❯ ljhack pull-cmr cos
(0.0) :=         prom <-[grafana_datasource]-> grafana
(0.1) :=         prom <-[grafana_dashboard]-> grafana
(1.0) :=         prom <-[prometheus_scrape]-> prometheus
(2.0) :=         trfk <-[ingress]-> alertmanager
(3.0) :=         trfk <-[ingress]-> catalogue
(4.0) :=         trfk <-[traefik_route]-> grafana
(5.0) :=         trfk <-[ingress_per_unit]-> loki
(6.0) :=         trfk <-[ingress_per_unit]-> prometheus
(6.1) :=         trfk <-[prometheus_scrape]-> prometheus
Pick a CMR [0.0/0.1/1.0/2.0/3.0/4.0/5.0/6.0/6.1] (0.0): 1.0
relating <this model>.prom:self-metrics-endpoint <-[prometheus_scrape]-> cos.prometheus:metrics-endpoint

img.png

imatrix

This subcommand is used to view and manage the Integration Matrix of a model, that is, for each application pair, the possible (not just the currently active) relations. Whether a relation is possible or not is determined based on whether the interface name matches, same as juju does.

Todo: extend this model to CMRs.

view

jhack imatrix view will pretty-print the Integration Matrix itself:

                            integration  v0.1
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ providers\requirers ┃ prom                    ┃ trfk                   ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
│ prom                │ -n/a-                   │ ┌────────────────────┐ │
│                     │                         │ │ - no interfaces -  │ │
│                     │                         │ └────────────────────┘ │
│ trfk                │ ┌─────────────────────┐ │ -n/a-                  │
│                     │ │ ingress_per_unit N  │ │                        │
│                     │ │ prometheus_scrape Y │ │                        │
│                     │ └─────────────────────┘ │                        │
└─────────────────────┴─────────────────────────┴────────────────────────┘

This representation should tell you:

  • there are two applications in this model: trfk and prom
  • trfk exposes two relation endpoints (as a provider) that have a potential of interfacing with prom (who is a requirer): ingress_per_unit and prometheus_scrape
  • of these two endpoints, only prometheus_scrape is presently active, i.e. trfk is related to prom via that endpoint.
  • prom provides no endpoints that trfk requires

TODO: allow ignoring the directionality of the relation: show shared interfaces for which both apps have the same role (since juju will allow relating over those... won't it?)

fill

jhack imatrix fill will try to cross-relate all applications with one another in all possible ways.

clear

jhack imatrix clear is the opposite of fill: it will attempt to remove all relations in the matrix.

Scenario

Jhack offers some scripts to work with Scenario.

snapshot

Snapshot's purpose is to gather the State data structure from a real, live charm running in some cloud your local juju client has access to. This is handy in case:

  • you want to write a test about the state the charm you're developing is currently in
  • your charm is bork or in some inconsistent state, and you want to write a test to check the charm will handle it correctly the next time around (aka regression testing)
  • you are new to Scenario and want to quickly get started with a real-life example.

Suppose you have a Juju model with a prometheus-k8s unit deployed as prometheus-k8s/0. If you type scenario snapshot prometheus-k8s/0, you will get a printout of the State object. Pipe that out into some file, import all you need from scenario, and you have a working State that you can Context.run events with.

You can also pass a --format flag to obtain instead:

  • a jsonified State data structure, for portability
  • a full-fledged pytest test case (with imports and all), where you only have to fill in the charm type and the event that you wish to trigger.

state-apply

If you write to disk a snapshot as json with scenario snapshot foo/0 --format json | state.json, you can then open it in an editor and make changes at will to the state data. For example you can change unit status, relation data, a stored state's value, or remove a deferred event.

Then you can type scenario state-apply foo/0 ./state.json and jhack will do its utmost to force-feed the modified state into the foo/0 unit.

It will call status-set, relation-set, state-set and even write files to disk, attempting to match the state you supplied as closely as possible.

Limitations

It's important to understand what state-apply cannot do:

  • change another application's data (i.e. a relation's remote_[app|unit]_data)
  • change its own leadership status
  • change a model's name, uuid, etc...
  • add or remove a relation, network, etc...

And many other things.

In a nutshell, state-apply can only do:

  • what the charm unit itself could do by making the appropriate hook tool calls
  • what the admin could do by using wisely juju exec, juju ssh, juju scp on the specific target unit alone

Future work

  • scenario state-apply to force-feed a unit a previously snapshotted State data structure.
  • forward-port all jhack replay functionality to a scenario.State backend.
    • scenario recorder install install on a unit a script to automatically snapshot the state before/after each event the unit receives.
    • scenario recorder download-db
    • scenario recorder replay
    • integrate with Theatre to see the graph expand in real time

eval

jhack eval is a command that allows you to evaluate simple one-line expressions in the context of a live charm (or multiple units thereof).

jhack eval myapp/0 self.model.relations

and you should see as output the current list of relations the charm has, for example

[<ops.model.Relation ingress:50>]

Similarly, if the charm has a method called _foobar, you could write:

jhack eval myapp/0 self._foobar() + 42

and see the result in your standard output.

Run the command with --help for additional options and configuration.

script

jhack script is a command that allows you to upload a python script to one or multiple live Juju units and running them with the charm instance as an argument.

# ./path/to/script.py
def main(charm):
    relations = charm.model.relations['bar']
    for relation in relations:
        print(relation.app, relation.data[relation.app]['key'])
        relation.data[relation.app]['other-key'] = 'value'

Now type:

jhack script myapp/0 --input ./path/to/script.py

and you should see as output the current value of the key application data setting. You will also see that the other-key has been updated to "value".

Run the command with --help for additional options and configuration.

charm lobotomy

Helpful when you have to endure an event storm, or when you want a charm to temporarily suspend event processing for whatever reason.

jhack charm lobotomy myapp/0 will temporarily disable event processing for the charm. This is a total lobotomy. Juju will not be aware anything out of the ordinary is going on, but the charm will ignore all events and be effectively disconnected from the Juju state machine and allow all events to go through transparently.

jhack charm lobotomy myapp myotherapp -e update-status -e foo-relation-changed will lobotomize all units of myapp and myotherapp, and selectively so: they only ignore those specific events, and let any other go through to the charm.

jhack charm lobotomy --all: will lobotomize all applications in the current model

At any time, you can do jhack lobotomy --plan to view in a handy table what, lobotomy-wise, the status of the model is.

img.png

To reverse a lobotomy, do jhack charm lobotomy myapp/0 --undo.

jhack charm lobotomy myapp myotherapp --undo: removes the lobotomy from all units of myapp and myotherapp.

jhack charm lobotomy --undo will delobotomize the whole model.

jhack's People

Contributors

pietropasotti avatar ca-scribner avatar dstathis avatar carlcsaposs-canonical avatar thanhphan1147 avatar marcoppenheimer avatar simondeziel avatar hypeitnow avatar barrettj12 avatar thp-canonical avatar jamesbeedy avatar awnns avatar benhoyt avatar shayancanonical avatar

Stargazers

Mateusz Kulewicz avatar KC avatar John Lettman avatar Ashley James avatar  avatar Thorsten Merten avatar Christopher Bartz avatar Tony Meyer avatar Andreia Velasco avatar bikalpa avatar Gauthier Leonard avatar  avatar  avatar Macduff avatar  avatar  avatar james_lin avatar Jon Seager avatar Taihsiang Ho avatar Salih Furkan Demirer avatar SCai avatar Partha Ghosh avatar Mustafa Kemal GILOR avatar Carlos Crisóstomo Vals avatar Maksim Beliaev avatar Jack Shaw avatar Erhan Sunar avatar Juan Pablo Noreña-Monsalve avatar Kouki Hama avatar Michael avatar Sudeep Bhandari avatar Utkarsh Bhatt avatar Gustavo Sanchez avatar Alex Balderson avatar  avatar Claudio 'Clauz' Pisa avatar Mehdi Bendriss avatar Jason Nucciarone avatar Mia Altieri avatar Vitaly Antonenko avatar  avatar Raúl Zamora Martínez avatar Will Fitch avatar Natasha Ho avatar endika avatar Simon Aronsson avatar Natalia Nowakowska avatar Marcelo Henrique Neppel avatar

Watchers

James Cloos avatar Christopher Bartz avatar  avatar  avatar  avatar

jhack's Issues

Permission denied when running jhack fire on lxd cloud

I was testing out jhack when I encountered this:

erik@frozen:~$ jhack fire nextcloud/1 update-status
[captured stderr: ]
load pubkey "/home/erik/.ssh/id_rsa": Permission denied
Connection to 10.51.45.6 closed.

Fired update-status on nextcloud/1.
erik@frozen:~$

jhack snap version

jhack 0.3.14 201 latest/stable ppasotti -

(snap) jhack reads juju status as empty

Output from jhack tail:

Traceback (most recent call last):
  File "/snap/jhack/124/bin/jhack", line 8, in <module>
    sys.exit(main())
  File "/snap/jhack/124/lib/python3.8/site-packages/jhack/main.py", line 138, in main
    app()
  File "/snap/jhack/124/lib/python3.8/site-packages/typer/main.py", line 214, in __call__
    return get_command(self)(*args, **kwargs)
  File "/snap/jhack/124/lib/python3.8/site-packages/click/core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "/snap/jhack/124/lib/python3.8/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "/snap/jhack/124/lib/python3.8/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/snap/jhack/124/lib/python3.8/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/snap/jhack/124/lib/python3.8/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "/snap/jhack/124/lib/python3.8/site-packages/typer/main.py", line 500, in wrapper
    return callback(**use_params)  # type: ignore
  File "/snap/jhack/124/lib/python3.8/site-packages/jhack/utils/tail_charms.py", line 919, in tail_events
    return _tail_events(
  File "/snap/jhack/124/lib/python3.8/site-packages/jhack/utils/tail_charms.py", line 974, in _tail_events
    targets = parse_targets(targets) if not files else (targets or [])
  File "/snap/jhack/124/lib/python3.8/site-packages/jhack/utils/tail_charms.py", line 105, in parse_targets
    return get_all_units()
  File "/snap/jhack/124/lib/python3.8/site-packages/jhack/utils/tail_charms.py", line 98, in get_all_units
    chain(*(app.get("units", ()) for app in status["applications"].values()))
KeyError: 'applications'

I used pdb.set_trace() here to check the values of: (by following this method)
juju_status(json=True) was {}
jhack.helpers.juju_version() was '3.0.0'
jhack.helpers.juju_models() was ''

In my local environment:
juju version was 2.9.37-ubuntu-amd64
Copy-pasting code from tail_charms.py into a python3 interpreter gave me:
raw was '2.9.37-ubuntu-amd64'
raw.split("-")[0] was '2.9.37'

I did run this command: sudo snap connect jhack:dot-local-share-juju snapd

I also tried refreshing the snap from latest/stable: 0.3.2 2022-11-02 (120) to latest/beta: 0.3.2 2022-11-22 (124) and latest/edge: 0.3.2 2022-11-24 (126) with the same issue.

Running on Google Compute Engine Ubuntu 22.04

GCE environment:
{
  "creationTimestamp": "2022-11-28T10:47:13.287-08:00",
  "description": "",
  "id": "3183457864939781342",
  "kind": "compute#instanceTemplate",
  "name": "dev-env",
  "properties": {
    "confidentialInstanceConfig": {
      "enableConfidentialCompute": false
    },
    "description": "",
    "scheduling": {
      "onHostMaintenance": "MIGRATE",
      "provisioningModel": "STANDARD",
      "automaticRestart": true,
      "instanceTerminationAction": "STOP",
      "maxRunDuration": {
        "seconds": "36000",
        "nanos": 0
      },
      "preemptible": false
    },
    "tags": {},
    "disks": [
      {
        "type": "PERSISTENT",
        "deviceName": "instance-template-1",
        "autoDelete": true,
        "index": 0,
        "boot": true,
        "kind": "compute#attachedDisk",
        "mode": "READ_WRITE",
        "initializeParams": {
          "sourceImage": "projects/ubuntu-os-cloud/global/images/ubuntu-2204-jammy-v20221123",
          "diskType": "pd-ssd",
          "diskSizeGb": "50"
        }
      }
    ],
    "networkInterfaces": [
      {
        "stackType": "IPV4_ONLY",
        "name": "nic0",
        "network": "projects/dev-env-369519/global/networks/default",
        "accessConfigs": [
          {
            "name": "External NAT",
            "type": "ONE_TO_ONE_NAT",
            "kind": "compute#accessConfig",
            "networkTier": "PREMIUM"
          }
        ],
        "kind": "compute#networkInterface"
      }
    ],
    "reservationAffinity": {
      "consumeReservationType": "ANY_RESERVATION"
    },
    "canIpForward": false,
    "keyRevocationActionType": "NONE",
    "machineType": "n2-standard-4",
    "metadata": {
      "fingerprint": "rhj2YiywKp4=",
      "kind": "compute#metadata",
      "items": [
        {
          "value": "#cloud-config\npackage_upgrade: true\npackages:\n  - gnome-keyring\n  - tox\nsnap:\n  commands:\n    - snap refresh\n    - snap install juju --classic --channel=2.9/stable\n    - snap install charmcraft --classic\n    - snap install lxd\n    - snap install microk8s --classic\n    - snap alias microk8s.kubectl kubectl\n    - snap install jhack\n    - snap connect jhack:dot-local-share-juju snapd\nruncmd:\n  - adduser ubuntu lxd\n  - newgrp lxd\n  - lxd init --auto\n  - lxc network set lxdbr0 ipv6.address none\n  - adduser ubuntu microk8s\n  - newgrp microk8s\n  - microk8s status --wait-ready\n  - microk8s enable dns\n  - kubectl rollout status -n kube-system -w --timeout=5m deployments/coredns\n  - microk8s enable hostpath-storage\n  - kubectl rollout status -n kube-system -w --timeout=5m deployments/hostpath-provisioner\n",
          "key": "user-data"
        },
        {
          "value": "ubuntu:ssh-ed25519 [redacted] ubuntu",
          "key": "ssh-keys"
        }
      ]
    },
    "shieldedVmConfig": {
      "enableSecureBoot": false,
      "enableVtpm": true,
      "enableIntegrityMonitoring": true
    },
    "shieldedInstanceConfig": {
      "enableSecureBoot": false,
      "enableVtpm": true,
      "enableIntegrityMonitoring": true
    },
    "displayDevice": {
      "enableDisplay": false
    }
  },
  "selfLink": "projects/dev-env-369519/global/instanceTemplates/dev-env"
}
Cloud-init:
#cloud-config
package_upgrade: true
packages:
  - gnome-keyring
  - tox
snap:
  commands:
    - snap refresh
    - snap install juju --classic --channel=2.9/stable
    - snap install charmcraft --classic
    - snap install lxd
    - snap install microk8s --classic
    - snap alias microk8s.kubectl kubectl
    - snap install jhack
    - snap connect jhack:dot-local-share-juju snapd
runcmd:
  - adduser ubuntu lxd
  - newgrp lxd
  - lxd init --auto
  - lxc network set lxdbr0 ipv6.address none
  - adduser ubuntu microk8s
  - newgrp microk8s
  - microk8s status --wait-ready
  - microk8s enable dns
  - kubectl rollout status -n kube-system -w --timeout=5m deployments/coredns
  - microk8s enable hostpath-storage
  - kubectl rollout status -n kube-system -w --timeout=5m deployments/hostpath-provisioner
Commands run as `ubuntu` user after cloud-init:
juju bootstrap microk8s micro --agent-version=2.9.29
juju bootstrap localhost lxd --agent-version=2.9.29
juju model-defaults logging-config='<root>=INFO; unit=DEBUG'
ssh-keygen -t ed25519
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
cat ~/.ssh/id_ed25519.pub
juju switch micro

# something along the lines of
juju add-model foo

vinfo for current folder

Would be super handy if vinfo worked for charm libs for the current folder, without needing to deploy the charm.

jhack edge fails to nuke model

When on jhack edge, a warning is emitted: something went wrong nuking designate;stdout=stderr=ERROR option provided but not defined: -y

And the model is not deleted.

jhack nuke designate
Are you sure you want to nuke:
	⚛ app 'designate' (designate)
	⚛ model 'designate'
Please confirm [Y/n]: Y
                                                                                                                                                                                                                                                         
                                                                                                                             ____                                                                                                                        
                                                                                                                     __,-~~/~    `---.                                                                                                                   
                                                                                                                   _/_,---(      ,    )                                                                                                                  
                                                                                                               __ /        <    /   )  \___                                                                                                              
                                                                                                - ------===;;;'====------------------===;;;===----- -  -                                                                                                 
                                                                                                                  \/  ~"~"~"~"~"~\~"~)~"/                                                                                                                
                                                                                                                  (_ (   \  (     >    \)                                                                                                                
                                                                                                                   \_( _ <         >_>'                                                                                                                  
                                                                                                                      ~ `-i' ::>|--"                                                                                                                     
                                                                                                                          I;|.|.|                                                                                                                        
                                                                                                                         <|i::|i|`.                                                                                                                      
                                                                                                                        (` ^'"`-' ")                                                                                                                     
                                                                                                                                                                                                                                                         
                                                                                                            ~]==⚛❯  designate (designate)  ⚛                                                                                                             
                                                                                                                  ~]==⚛❯  designate  ⚛                                                                                                                   
something went wrong nuking designate;stdout=stderr=ERROR option provided but not defined: -y

                                                                                                   ~]==⚛❯ app 'designate' (designate) still in flight                                                                                                    
                                                                                                                         ✞ RIP ✞ 

Version

Version installed: 0.3.19.1.2 2023-09-04 (241)
Juju: 3.1.5 (23354)

More details

The operation succeeds on latest/stable

jhack could not get current model

It seems that jack has a problem finding the name of the current model. I have seen this error with multiple commands.

x1:➜  ~ jhack fire ubuntu/0 update-status                   
Traceback (most recent call last):                                                                                                          
  File "/snap/jhack/135/bin/jhack", line 8, in <module>    
    sys.exit(main())   
  File "/snap/jhack/135/lib/python3.8/site-packages/jhack/main.py", line 140, in main
    app()                                                                                                                                   
  File "/snap/jhack/135/lib/python3.8/site-packages/typer/main.py", line 214, in __call__
    return get_command(self)(*args, **kwargs)
  File "/snap/jhack/135/lib/python3.8/site-packages/click/core.py", line 1130, in __call__
    return self.main(*args, **kwargs)                                                                                                       
  File "/snap/jhack/135/lib/python3.8/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "/snap/jhack/135/lib/python3.8/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/snap/jhack/135/lib/python3.8/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/snap/jhack/135/lib/python3.8/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "/snap/jhack/135/lib/python3.8/site-packages/typer/main.py", line 500, in wrapper
    return callback(**use_params)  # type: ignore
  File "/snap/jhack/135/lib/python3.8/site-packages/jhack/utils/simulate_event.py", line 191, in simulate_event
    return _simulate_event(                                           
  File "/snap/jhack/135/lib/python3.8/site-packages/jhack/utils/simulate_event.py", line 122, in _simulate_event
    env = _get_env(                                                                                                                         
  File "/snap/jhack/135/lib/python3.8/site-packages/jhack/utils/simulate_event.py", line 52, in _get_env
    "JUJU_MODEL_NAME": current_model(),
  File "/snap/jhack/135/lib/python3.8/site-packages/jhack/helpers.py", line 187, in current_model
    return next(filter(key, all_models)).strip("*")
StopIteration                                                                                                                        [1.66s]

x1:➜  ~ juju models
Controller: u1

Model        Cloud/Region  Type  Status     Machines  Units  Access  Last connection
controller   u1-lxd/u1     lxd   available         1      -  admin   just now
default      u1-lxd/u1     lxd   available         0      -  admin   2023-01-20
juju-spell*  u1-lxd/u1     lxd   available         2      4  admin   4 seconds ago
                                                                                                                                     [1.02s]
x1:➜  ~ snap info jhack
name:      jhack
summary:   Make charming charming again.
publisher: Pietro Pasotti (ppasotti)
store-url: https://snapcraft.io/jhack
contact:   [email protected]
license:   unset
description: |
  An opinionated collection of scripts and utilities for charmers.
  
  In order to use the cli tools that hit the juju api, you'll need to connect the
  dot-local-share-juju interface.
  
  Command: `sudo snap connect jhack:dot-local-share-juju snapd`
  
  Jhack needs that interface to obtain WRITE access to `~/.local/share/juju`, which is the folder
  where your local juju client stores its state, including:
    - cloud credentials
    - what controllers are registered, which controller is current
    - what models are present on each controller, which model is current
  
  If you see errors like:
  
      ERROR cannot load ssh client keys: open /home/pietro/.local/share/juju/ssh: permission denied
      No relations found in model None.
  
  Then you probably forgot to connect that plug.
commands:
  - jhack
snap-id:      gnxWJ9Sdlew4qfduWHEW8oczvdo2mvTT
tracking:     latest/candidate
refresh-date: today at 10:13 CET
channels:
  latest/stable:    0.3.2 2022-11-02 (120) 99MB -
  latest/candidate: 0.3.4 2023-01-02 (135) 99MB -
  latest/beta:      0.3.4 2023-01-02 (135) 99MB -
  latest/edge:      0.3.7 2023-01-25 (166) 99MB -
installed:          0.3.4            (135) 99MB -                                                                                    [0.27s]

jhack snap should source jhack from the local python project

Currently the jhack snap sources jhack from github instead of from the project files on the local filesystem. This prevents users from easily building dev builds of the jhack snap locally.

To fix this, the jhack snap should install jhack from the local files.

jhack sync isnt syncing

I made some local changes and ran "jhack sync"

erik@frozen:~/nextcloud-charms/operator-nextcloud$ jhack sync nextcloud/0 
WARNING:jhack./snap/jhack/201/lib/python3.8/site-packages/jhack/utils/sync.py:not a directory: cannot watch /home/erik/nextcloud-charms/operator-nextcloud/lib. Skipping...
watching: 
	/home/erik/nextcloud-charms/operator-nextcloud/src/interface_http.py
	/home/erik/nextcloud-charms/operator-nextcloud/src/interface_mount.py
	/home/erik/nextcloud-charms/operator-nextcloud/src/occ.py
	/home/erik/nextcloud-charms/operator-nextcloud/src/interface_redis.py
	/home/erik/nextcloud-charms/operator-nextcloud/src/utils.py
	/home/erik/nextcloud-charms/operator-nextcloud/src/charm.py
Ctrl+C to interrupt
synced /home/erik/nextcloud-charms/operator-nextcloud/src/charm.py -> nextcloud/0
synced /home/erik/nextcloud-charms/operator-nextcloud/src/charm.py -> nextcloud/0
synced /home/erik/nextcloud-charms/operator-nextcloud/src/charm.py -> nextcloud/0
synced /home/erik/nextcloud-charms/operator-nextcloud/src/charm.py -> nextcloud/0

But when I looka at the file on the unit, its not changed at all.

nuke fails with exception

If you run nuke, you get an exception. However, this is an expected error (OR?)

if yes, then traceback should be truncated and only error message left

ubuntu@charm-dev:~$ jhack nuke
ERROR:jhack./snap/jhack/120/lib/python3.8/site-packages/jhack/utils/tail_charms.py:failed to determine model loglevel: not enough values to unpack (expected 2, got 1). Guessing `WARNING` for now.
ERROR:jhack:It seems like the snap doesn't have access to /home/ubuntu/.local/share/juju;to grant it, run 'sudo snap connect jhack:dot-local-share-juju snapd'.Some Jhack commands will still work, but those that interact with the juju client will not.
Traceback (most recent call last):
  File "/snap/jhack/120/bin/jhack", line 8, in <module>
    sys.exit(main())
  File "/snap/jhack/120/lib/python3.8/site-packages/jhack/main.py", line 134, in main
    app()
  File "/snap/jhack/120/lib/python3.8/site-packages/typer/main.py", line 214, in __call__
    return get_command(self)(*args, **kwargs)
  File "/snap/jhack/120/lib/python3.8/site-packages/click/core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "/snap/jhack/120/lib/python3.8/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "/snap/jhack/120/lib/python3.8/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/snap/jhack/120/lib/python3.8/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/snap/jhack/120/lib/python3.8/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "/snap/jhack/120/lib/python3.8/site-packages/typer/main.py", line 500, in wrapper
    return callback(**use_params)  # type: ignore
  File "/snap/jhack/120/lib/python3.8/site-packages/jhack/utils/nuke.py", line 447, in nuke
    _nuke(None, **kwargs)
  File "/snap/jhack/120/lib/python3.8/site-packages/jhack/utils/nuke.py", line 218, in _nuke
    nukeables = [Nukeable(current_model(), "model")]
  File "/snap/jhack/120/lib/python3.8/site-packages/jhack/helpers.py", line 166, in current_model
    return next(filter(key, all_models)).strip("*")
StopIteration

jhack list-endpoints string indices must be integers

Trying the new command to list endpoints failed with: string indices must be integers

ubuntu@bm0:~$ jhack list-endpoints keystone
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /snap/jhack/227/lib/python3.8/site-packages/jhack/utils/list_endpoints.py:108 in list_endpoints  │
│                                                                                                  │
│   105 │   color: Optional[str] = ColorOption,                                                    │
│   106 ):                                                                                         │
│   107 │   """Display the available integration endpoints."""                                     │
│ ❱ 108 │   _list_endpoints(app=app, model=model, show_versions=show_versions, color=color)        │
│   109                                                                                            │
│   110                                                                                            │
│   111 if __name__ == "__main__":                                                                 │
│                                                                                                  │
│ ╭────────── locals ──────────╮                                                                   │
│ │           app = 'keystone' │                                                                   │
│ │         color = 'auto'     │                                                                   │
│ │         model = None       │                                                                   │
│ │ show_versions = False      │                                                                   │
│ ╰────────────────────────────╯                                                                   │
│                                                                                                  │
│ /snap/jhack/227/lib/python3.8/site-packages/jhack/utils/list_endpoints.py:90 in _list_endpoints  │
│                                                                                                  │
│    87 │   libinfo = get_libinfo(app, model) if show_versions else None                           │
│    88 │                                                                                          │
│    89 │   c = Console(color_system=color)                                                        │
│ ❱  90 │   c.print(_render(endpoints, libinfo))                                                   │
│    91                                                                                            │
│    92                                                                                            │
│    93 def list_endpoints(                                                                        │
│                                                                                                  │
│ ╭──────────────────────────────────────── locals ─────────────────────────────────────────╮      │
│ │           app = 'keystone'                                                              │      │
│ │             c = <console width=248 ColorSystem.EIGHT_BIT>                               │      │
│ │         color = 'auto'                                                                  │      │
│ │     endpoints = {                                                                       │      │
│ │                 │   'requires': {                                                       │      │
│ │                 │   │   'database': ('mysql_client', ['keystone-mysql-router']),        │      │
│ │                 │   │   'ingress-internal': ('ingress', ['traefik']),                   │      │
│ │                 │   │   'ingress-public': ('ingress', ['traefik'])                      │      │
│ │                 │   },                                                                  │      │
│ │                 │   'provides': {                                                       │      │
│ │                 │   │   'identity-service': (                                           │      │
│ │                 │   │   │   'keystone',                                                 │      │
│ │                 │   │   │   ['cinder', 'glance', 'neutron', 'nova', 'placement']        │      │
│ │                 │   │   ),                                                              │      │
│ │                 │   │   'identity-credentials': ('keystone-credentials', ['horizon'])   │      │
│ │                 │   },                                                                  │      │
│ │                 │   'peers': [                                                          │      │
│ │                 │   │   PeerBinding(endpoint='peers', interface='keystone-peer')        │      │
│ │                 │   ]                                                                   │      │
│ │                 }                                                                       │      │
│ │       libinfo = None                                                                    │      │
│ │         model = None                                                                    │      │
│ │ show_versions = False                                                                   │      │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────╯      │
│                                                                                                  │
│ /snap/jhack/227/lib/python3.8/site-packages/jhack/utils/list_endpoints.py:59 in _render          │
│                                                                                                  │
│    56 │   │   │   │   │   │   if libinfo                                                         │
│    57 │   │   │   │   │   │   else []                                                            │
│    58 │   │   │   │   │   )                                                                      │
│ ❱  59 │   │   │   │   │   + [", ".join(remote["related-application"] for remote in remotes)]     │
│    60 │   │   │   │   ),                                                                         │
│    61 │   │   │   )                                                                              │
│    62 │   │   │   first = False                                                                  │
│                                                                                                  │
│ ╭───────────────────────────────────────── locals ─────────────────────────────────────────╮     │
│ │  endpoint_name = 'database'                                                              │     │
│ │      endpoints = {                                                                       │     │
│ │                  │   'requires': {                                                       │     │
│ │                  │   │   'database': ('mysql_client', ['keystone-mysql-router']),        │     │
│ │                  │   │   'ingress-internal': ('ingress', ['traefik']),                   │     │
│ │                  │   │   'ingress-public': ('ingress', ['traefik'])                      │     │
│ │                  │   },                                                                  │     │
│ │                  │   'provides': {                                                       │     │
│ │                  │   │   'identity-service': (                                           │     │
│ │                  │   │   │   'keystone',                                                 │     │
│ │                  │   │   │   ['cinder', 'glance', 'neutron', 'nova', 'placement']        │     │
│ │                  │   │   ),                                                              │     │
│ │                  │   │   'identity-credentials': ('keystone-credentials', ['horizon'])   │     │
│ │                  │   },                                                                  │     │
│ │                  │   'peers': [                                                          │     │
│ │                  │   │   PeerBinding(endpoint='peers', interface='keystone-peer')        │     │
│ │                  │   ]                                                                   │     │
│ │                  }                                                                       │     │
│ │          first = True                                                                    │     │
│ │ interface_name = 'mysql_client'                                                          │     │
│ │        libinfo = None                                                                    │     │
│ │        remotes = ['keystone-mysql-router']                                               │     │
│ │           role = 'requires'                                                              │     │
│ │          table = <rich.table.Table object at 0x7f0ee0a3d100>                             │     │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────╯     │
│                                                                                                  │
│ /snap/jhack/227/lib/python3.8/site-packages/jhack/utils/list_endpoints.py:59 in <genexpr>        │
│                                                                                                  │
│    56 │   │   │   │   │   │   if libinfo                                                         │
│    57 │   │   │   │   │   │   else []                                                            │
│    58 │   │   │   │   │   )                                                                      │
│ ❱  59 │   │   │   │   │   + [", ".join(remote["related-application"] for remote in remotes)]     │
│    60 │   │   │   │   ),                                                                         │
│    61 │   │   │   )                                                                              │
│    62 │   │   │   first = False                                                                  │
│                                                                                                  │
│ ╭───────────────────── locals ──────────────────────╮                                            │
│ │     .0 = <list_iterator object at 0x7f0ee0a3d340> │                                            │
│ │ remote = 'keystone-mysql-router'                  │                                            │
│ ╰───────────────────────────────────────────────────╯                                            │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
TypeError: string indices must be integers

Examples for nuke are not properly formatted

After running jhack nuke --help, I can see that there are several usage examples, but they are not properly formatted and thus difficult to read.

x1:➜  ~ jhack nuke --help   
Usage: jhack nuke [OPTIONS] [WHAT]...

  Surgical carpet bombing tool.

  Attempts to guess what you want to burn, and rains holy vengeance upon it.

  Examples:     $ jhack nuke     will vanquish the current model     $ jhack
  nuke test-foo-*     will bomb all nukeables starting with `test-foo-` ,
  including:      - models      - applications      - relations     $ jhack
  nuke --model foo bar-*     will bomb all nukeables starting with `bar-` in
  model foo. As above.     $ jhack nuke -n=2 *foo*     will blow up the two
  things it can find that contain the substring "foo"

  Nuke ascii art by Bill March from https://www.asciiart.eu/weapons/explosives

Arguments:
  [WHAT]...  What to ⚛.

Options:
  -s, --select TEXT     Selector specifiers to choose what to ⚛.A lower-case
                        letter indicates `include`, an upper-case one
                        indicates `exclude`.
                        
                        m := models; a := apps; r := relations
                        
                        Examples:`ma` = only include models and apps in the
                        target selection (equivalent to `R`). `AR` = exclude
                        models and relations (equivalent to `m`)
  -m, --model TEXT      The model. Defaults to current model.
  -n, --number INTEGER  Exact number of things you're expectig to get
                        nuked.Safety first.
  -b, --borked          Nukes all borked applications in current or target
                        model.
  --dry-run             Do nothing, print out what would have happened.
  -c, --color TEXT      Color scheme to adopt. Supported options: ['auto',
                        'standard', '256', 'truecolor', 'windows', 'no'] no:
                        disable colors entirely.  [default: auto]
  --help                Show this message and exit.                                                                                  [1.33s]
x1:➜  ~ snap info jhack
name:      jhack
summary:   Make charming charming again.
publisher: Pietro Pasotti (ppasotti)
store-url: https://snapcraft.io/jhack
contact:   [email protected]
license:   unset
description: |
  An opinionated collection of scripts and utilities for charmers.
  
  In order to use the cli tools that hit the juju api, you'll need to connect the
  dot-local-share-juju interface.
  
  Command: `sudo snap connect jhack:dot-local-share-juju snapd`
  
  Jhack needs that interface to obtain WRITE access to `~/.local/share/juju`, which is the folder
  where your local juju client stores its state, including:
    - cloud credentials
    - what controllers are registered, which controller is current
    - what models are present on each controller, which model is current
  
  If you see errors like:
  
      ERROR cannot load ssh client keys: open /home/pietro/.local/share/juju/ssh: permission denied
      No relations found in model None.
  
  Then you probably forgot to connect that plug.
commands:
  - jhack
snap-id:      gnxWJ9Sdlew4qfduWHEW8oczvdo2mvTT
tracking:     latest/candidate
refresh-date: today at 10:13 CET
channels:
  latest/stable:    0.3.2 2022-11-02 (120) 99MB -
  latest/candidate: 0.3.4 2023-01-02 (135) 99MB -
  latest/beta:      0.3.4 2023-01-02 (135) 99MB -
  latest/edge:      0.3.7 2023-01-25 (166) 99MB -
installed:          0.3.4            (135) 99MB -                                                                                    [0.40s]

jhack does not see folders in root if installed from snap

Issue:

/fastapi-demo/charm$ jhack utils sync ./src demo-api-charm/0
ERROR:jhack./snap/jhack/97/lib/python3.8/site-packages/jhack/utils/sync.py:not a directory: /var/lib/snapd/void/src; cannot watch.
ERROR:jhack./snap/jhack/97/lib/python3.8/site-packages/jhack/utils/sync.py:nothing to watch

that comes that snap does not have access to the root:

ubuntu@charm-dev:/fastapi-demo/charm$ snap run --shell jhack
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

ubuntu@charm-dev:/var/lib/snapd/void$ ls /
bin  boot  dev  etc  home  host  lib  lib32  lib64  libx32  media  meta  mnt  opt  proc  root  run  sbin  snap  srv  sys  tmp  usr  var  writable

while on host:

ubuntu@charm-dev:/fastapi-demo/charm$ ls /
bin  boot  dev  etc  fastapi-demo  home  lib  lib32  lib64  libx32  lost+found  media  mnt  opt  proc  root  run  sbin  snap  srv  sys  tmp  usr  var

Machine charm detection does not work

In ./helpers.py, get_substrate() is defined as:

def get_substrate(model: str = None) -> Literal["k8s", "machine"]:
    """Attempts to guess whether we're talking k8s or machine."""
    cmd = f'juju show-model{f" {model}" if model else ""} --format=json'
    proc = JPopen(cmd.split())
    raw = proc.stdout.read().decode("utf-8")
    model_info = jsn.loads(raw)

    if not model:
        model = list(model_info)[0]

    model_type = model_info[model]["model-type"]
    if model_type == "caas":
        return "machine"
    elif model_type == "iaas":
        return "k8s"
    else:
        raise ValueError(f"unrecognized model type: {model_type}")

This does not work in practice, because our machine charms also report model-type as "iaas". A field that should set them apart is type which says "lxd" for us but I'm not sure I have all bases covered in terms of which values are possible here so I will wait a little with contributing a fix.

imatrix doesn't work on machine models

dylan@protostar:~/agent$ jhack imatrix view
load pubkey "/home/dylan/.ssh/id_rsa": Permission denied
Connection to 10.152.136.3 closed.
Traceback (most recent call last):
  File "/snap/jhack/120/bin/jhack", line 8, in <module>
    sys.exit(main())
  File "/snap/jhack/120/lib/python3.8/site-packages/jhack/main.py", line 134, in main
    app()
  File "/snap/jhack/120/lib/python3.8/site-packages/typer/main.py", line 214, in __call__
    return get_command(self)(*args, **kwargs)
  File "/snap/jhack/120/lib/python3.8/site-packages/click/core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "/snap/jhack/120/lib/python3.8/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "/snap/jhack/120/lib/python3.8/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/snap/jhack/120/lib/python3.8/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/snap/jhack/120/lib/python3.8/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/snap/jhack/120/lib/python3.8/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "/snap/jhack/120/lib/python3.8/site-packages/typer/main.py", line 500, in wrapper
    return callback(**use_params)  # type: ignore
  File "/snap/jhack/120/lib/python3.8/site-packages/jhack/utils/integrate.py", line 360, in show
    mtrx = IntegrationMatrix(apps=apps, model=model, color=color)
  File "/snap/jhack/120/lib/python3.8/site-packages/jhack/utils/integrate.py", line 72, in __init__
    self._endpoints = _gather_endpoints(model, apps)
  File "/snap/jhack/120/lib/python3.8/site-packages/jhack/utils/integrate.py", line 47, in _gather_endpoints
    unit = next(iter(app["units"]))
KeyError: 'units'

imatrix view reports no established relation (in contradiction to juju status)

Hi Pietro,

Additionally to imatrix improvements requested in #47, I want to report an issue with "imatrix view".
See the full logs below, juju reports established relation pgbouncer-k8s:db <> finos-waltz-k8s:db but "imatrix view" said "pgsql N":

> juju status --relations
Model     Controller  Cloud/Region        Version  SLA          Timestamp
welcome2  charm-dev   microk8s/localhost  2.9.38   unsupported  12:20:23+01:00

App                        Version  Status  Scale  Charm                      Channel      Rev  Address         Exposed  Message
finos-waltz-k8s                     active      1  finos-waltz-k8s            edge          28  10.152.183.179  no       
pgbouncer-k8s                       active      2  pgbouncer-k8s              latest/edge   20  10.152.183.75   no       
postgresql-k8s                      active      2  postgresql-k8s             latest/edge   44  10.152.183.36   no       
tls-certificates-operator           active      1  tls-certificates-operator  latest/beta   22  10.152.183.254  no       

Unit                          Workload  Agent  Address      Ports  Message
finos-waltz-k8s/0*            active    idle   10.1.84.112         
pgbouncer-k8s/0*              active    idle   10.1.84.106         
pgbouncer-k8s/1               active    idle   10.1.84.105         
postgresql-k8s/0              active    idle   10.1.84.110         
postgresql-k8s/1*             active    idle   10.1.84.109         Primary
tls-certificates-operator/0*  active    idle   10.1.84.111         

Relation provider                       Requirer                            Interface                 Type     Message
pgbouncer-k8s:db                        finos-waltz-k8s:db                  pgsql                     regular  <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
pgbouncer-k8s:pgb-peers                 pgbouncer-k8s:pgb-peers             pgb_peers                 peer     
postgresql-k8s:database                 pgbouncer-k8s:backend-database      postgresql_client         regular  
postgresql-k8s:database-peers           postgresql-k8s:database-peers       postgresql_peers          peer     
postgresql-k8s:restart                  postgresql-k8s:restart              rolling_op                peer     
tls-certificates-operator:certificates  pgbouncer-k8s:certificates          tls-certificates          regular  
tls-certificates-operator:certificates  postgresql-k8s:certificates         tls-certificates          regular  
tls-certificates-operator:replicas      tls-certificates-operator:replicas  tls-certificates-replica  peer     

> jhack imatrix view
                                                                                              integration  v0.1                                                                                               
                                    ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓                                     
                                    ┃ providers\requirers       ┃ finos-waltz-k8s        ┃ pgbouncer-k8s           ┃ postgresql-k8s         ┃ tls-certificates-operator ┃                                     
                                    ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩                                     
                                    │ finos-waltz-k8s           │ -n/a-                  │ ┏━━━━━━━━━━━━━━━━━━━━━┓ │ ┏━━━━━━━━━━━━━━━━━━━━┓ │ ┏━━━━━━━━━━━━━━━━━━━━━━━┓ │                                     
                                    │                           │                        │ ┃ - no interfaces -   ┃ │ ┃ - no interfaces -  ┃ │ ┃ - no interfaces -     ┃ │                                     
                                    │                           │                        │ └─────────────────────┘ │ └────────────────────┘ │ └───────────────────────┘ │                                     
                                    │ pgbouncer-k8s             │ ┏━━━━━━━━━━━━━━━━━━━━┓ │ -n/a-                   │ ┏━━━━━━━━━━━━━━━━━━━━┓ │ ┏━━━━━━━━━━━━━━━━━━━━━━━┓ │                                     
>>>>>>                              │                           │ ┃ pgsql N            ┃ │                         │ ┃ - no interfaces -  ┃ │ ┃ - no interfaces -     ┃ │                                     
                                    │                           │ └────────────────────┘ │                         │ └────────────────────┘ │ └───────────────────────┘ │                                     
                                    │ postgresql-k8s            │ ┏━━━━━━━━━━━━━━━━━━━━┓ │ ┏━━━━━━━━━━━━━━━━━━━━━┓ │ -n/a-                  │ ┏━━━━━━━━━━━━━━━━━━━━━━━┓ │                                     
                                    │                           │ ┃ pgsql N            ┃ │ ┃ postgresql_client Y ┃ │                        │ ┃ - no interfaces -     ┃ │                                     
                                    │                           │ └────────────────────┘ │ └─────────────────────┘ │                        │ └───────────────────────┘ │                                     
                                    │ tls-certificates-operator │ ┏━━━━━━━━━━━━━━━━━━━━┓ │ ┏━━━━━━━━━━━━━━━━━━━━━┓ │ ┏━━━━━━━━━━━━━━━━━━━━┓ │ -n/a-                     │                                     
                                    │                           │ ┃ - no interfaces -  ┃ │ ┃ tls-certificates Y  ┃ │ ┃ tls-certificates Y ┃ │                           │                                     
                                    │                           │ └────────────────────┘ │ └─────────────────────┘ │ └────────────────────┘ │                           │                                     
                                    └───────────────────────────┴────────────────────────┴─────────────────────────┴────────────────────────┴───────────────────────────┘                

The latest edge version 0.3.10 (revision 186) has the same issue:

> jhack imatrix view
                                                                                              integration  v0.1                                                                                               
                               ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓                                
                               ┃ providers\requirers       ┃ finos-waltz-k8s        ┃ pgbouncer-k8s                ┃ postgresql-k8s              ┃ tls-certificates-operator ┃                                
                               ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩                                
                               │ finos-waltz-k8s           │ -n/a-                  │ ┌──────────────────────────┐ │ ┌─────────────────────────┐ │ ┌───────────────────────┐ │                                
                               │                           │                        │ │ - no interfaces -        │ │ │ - no interfaces -       │ │ │ - no interfaces -     │ │                                
                               │                           │                        │ └──────────────────────────┘ │ └─────────────────────────┘ │ └───────────────────────┘ │                                
                               │ pgbouncer-k8s             │ ┌────────────────────┐ │ -n/a-                        │ ┌─────────────────────────┐ │ ┌───────────────────────┐ │                                
 >>>>>                         │                           │ │ pgsql (1x) N       │ │                              │ │ - no interfaces -       │ │ │ - no interfaces -     │ │                                
                               │                           │ └────────────────────┘ │                              │ └─────────────────────────┘ │ └───────────────────────┘ │                                
                               │ postgresql-k8s            │ ┌────────────────────┐ │ ┌──────────────────────────┐ │ -n/a-                       │ ┌───────────────────────┐ │                                
                               │                           │ │ pgsql (1x) N       │ │ │ postgresql_client (1x) Y │ │                             │ │ - no interfaces -     │ │                                
                               │                           │ └────────────────────┘ │ └──────────────────────────┘ │                             │ └───────────────────────┘ │                                
                               │ tls-certificates-operator │ ┌────────────────────┐ │ ┌──────────────────────────┐ │ ┌─────────────────────────┐ │ -n/a-                     │                                
                               │                           │ │ - no interfaces -  │ │ │ tls-certificates (1x) Y  │ │ │ tls-certificates (1x) Y │ │                           │                                
                               │                           │ └────────────────────┘ │ └──────────────────────────┘ │ └─────────────────────────┘ │                           │                                
                               └───────────────────────────┴────────────────────────┴──────────────────────────────┴─────────────────────────────┴───────────────────────────┘                                

It could be related to multiply legacy interfaces in pgbouncer-k8s:

provides:
...
  # Legacy relations - these will be deprecated in a future release
  db:
    interface: pgsql
    optional: true
  db-admin:
    interface: pgsql
    optional: true

Juju 2.9.38 snap 2.9/stable (rev 21790), Microk8s 1.26.1/stable (rev 4595).

Tnx for the nice tool!

Can't get relation databag in a CMR

I'm trying to get the content of a CMR relationship with jhack, and can't seem to be able to:
When I try to, I get this error, which seems quite clear to me:

jhack show-relation cinder-ceph:ceph microceph:ceph
RuntimeError: No relation found with endpoints 'cinder-ceph:ceph' -> 'microceph:ceph' in model '<the current model>'. 'cinder-ceph' not found in model '<the current model>'; 
are you trying to show a CMR? If so, you need to 
pass `-m <the model where 'cinder-ceph' lives>`

But then:

jhack show-relation -m openstack cinder-ceph:ceph microceph:ceph
KeyError: 'microceph'

Looks like jhack is trying to get the status of microceph in the current model

│ ╭────────────── locals ───────────────╮                                                          │
│ │       endpoint = 'microceph:ceph'   │                                                          │
│ │          model = 'openstack'        │                                                          │
│ │ other_endpoint = 'cinder-ceph:ceph' │                                                          │
│ ╰─────────────────────────────────────╯                                                          │
│                                                                                                  │
│ /snap/jhack/225/lib/python3.8/site-packages/jhack/utils/show_relation.py:260 in                  │
│ get_metadata_from_status                                                                         │
│                                                                                                  │
│    257 ):                                                                                        │
│    258 │   status = _juju_status(model=model, json=True)                                         │
│    259 │   # machine status json output apparently has no 'scale'... -_-                         │
│ ❱  260 │   app_status = status["applications"][endpoint.app_name]

Versions:

juju                  3.2.0          23190  3.2/stable          canonical✓  -
jhack                 0.3.18.3       225    latest/edge         ppasotti    -

make jhack work with beta juju releases

Running jhack and using juju from latest/edge gives an error (from what looks like some version or tag parsing).

$ jhack
Traceback (most recent call last):
  File "/snap/jhack/124/bin/jhack", line 5, in <module>
    from jhack.main import main
  File "/snap/jhack/124/lib/python3.8/site-packages/jhack/main.py", line 34, in <module>
    from jhack.utils.event_recorder.client import (
  File "/snap/jhack/124/lib/python3.8/site-packages/jhack/utils/event_recorder/client.py", line 19, in <module>
    from jhack.utils.simulate_event import _simulate_event
  File "/snap/jhack/124/lib/python3.8/site-packages/jhack/utils/simulate_event.py", line 9, in <module>
    _J_EXEC_CMD = "juju-exec" if juju_agent_version() >= (3, 0) else "juju-run"
  File "/snap/jhack/124/lib/python3.8/site-packages/jhack/helpers.py", line 145, in juju_agent_version
    return tuple(map(int, agent_version.split(".")))
ValueError: invalid literal for int() with base 10: '1-beta1'
$ snap info jhack
name:      jhack
summary:   Make charming charming again.
publisher: Pietro Pasotti (ppasotti)
store-url: https://snapcraft.io/jhack
contact:   [email protected]
license:   unset
description: |
  An opinionated collection of scripts and utilities for charmers.
  
  In order to use the cli tools that hit the juju api, you'll need to connect the
  dot-local-share-juju interface.
  
  Command: `sudo snap connect jhack:dot-local-share-juju snapd`
  
  Jhack needs that interface to obtain WRITE access to `~/.local/share/juju`, which is the folder
  where your local juju client stores its state, including:
    - cloud credentials
    - what controllers are registered, which controller is current
    - what models are present on each controller, which model is current
  
  If you see errors like:
  
      ERROR cannot load ssh client keys: open /home/pietro/.local/share/juju/ssh: permission denied
      No relations found in model None.
  
  Then you probably forgot to connect that plug.
commands:
  - jhack
snap-id:      gnxWJ9Sdlew4qfduWHEW8oczvdo2mvTT
tracking:     latest/candidate
refresh-date: 14 days ago, at 23:01 UTC
channels:
  latest/stable:    0.3.2 2022-11-02 (120) 99MB -
  latest/candidate: 0.3.2 2022-11-22 (124) 99MB -
  latest/beta:      0.3.2 2022-11-22 (124) 99MB -
  latest/edge:      0.3.3 2022-12-02 (133) 99MB -
installed:          0.3.2            (124) 99MB -
[localhost-localhost:controller]
bdx@raton00:~/allcode/github/omnivector/slurm-bundles
$ snap info juju
name:      juju
summary:   Juju - a model-driven operator lifecycle manager for K8s and machines
publisher: Canonical✓
store-url: https://snapcraft.io/juju
contact:   https://canonical.com/
license:   AGPL-3.0
description: |
  A model-driven **universal operator lifecycle manager** for multi cloud and hybrid cloud
  application management on K8s and machines.
  
  **What is an operator lifecycle manager?**
  Kubernetes operators are containers with operations code, that drive your applications on K8s.
  Juju is an operator lifecycle manager that manages the installation, integration and configuration
  of operators on the cluster. Juju also extends the idea of operators to traditional application
  management on Linux and Windows servers, or cloud instances.
  
  **Model-driven operations and integration**
  Organise your operators into models, which group together applications that can be tightly
  integrated on the same substrate and operated by the same team. Capture resource allocation,
  storage, networking and integration information in the model to simplify ongoing operations.
  
  **Better day-2 operations**
  Each operator code package, called a charm, declares methods for actions like back, restore, or
  security audit. Calling these methods provides remote administration of the application with no
  low-level access required.
  
  **Learn more**
  
   - https://juju.is/
   - https://discourse.charmhub.io/
   - https://github.com/juju/juju
commands:
  - juju
services:
  juju.fetch-oci: oneshot, disabled, inactive
snap-id:      e2CPHpB1fUxcKtCyJTsm5t3hN9axJ0yj
tracking:     latest/edge
refresh-date: today at 18:08 UTC
channels:
  2.9/stable:       2.9.37            2022-11-14 (21315) 96MB classic
  2.9/candidate:    ↑                                         
  2.9/beta:         ↑                                         
  2.9/edge:         2.9.38-4985b7c    2022-12-05 (21630) 97MB classic
  latest/stable:    2.9.37            2022-11-14 (21315) 96MB classic
  latest/candidate: ↑                                         
  latest/beta:      ↑                                         
  latest/edge:      3.1-beta1-565f2ac 2022-12-05 (21634) 77MB -
  3.1/stable:       –                                         
  3.1/candidate:    –                                         
  3.1/beta:         –                                         
  3.1/edge:         3.1-beta1-565f2ac 2022-12-05 (21634) 77MB -
  3.0/stable:       3.0.2             2022-12-01 (21474) 76MB -
  3.0/candidate:    ↑                                         
  3.0/beta:         ↑                                         
  3.0/edge:         3.0.3-10fda27     2022-12-05 (21638) 76MB -
  2.8/stable:       2.8.13            2021-11-11 (17665) 74MB classic
  2.8/candidate:    ↑                                         
  2.8/beta:         ↑                                         
  2.8/edge:         ↑                                         
  2.7/stable:       2.7.8             2020-07-22 (13563) 77MB classic
  2.7/candidate:    ↑                                         
  2.7/beta:         ↑                                         
  2.7/edge:         ↑                                         
installed:          3.1-beta1-565f2ac            (21634) 77MB -

jenv only works when installed with '-e'

jenv reports the jhack version by reading pyproject.toml. In a normal installation, the file does not exist.

A solution that should work would be to add a MANIFEST.in file. See cos-alerter for an example. Never mind this doesn't work because the file is outside the package directory.

jhack crashes on machine without juju bootstrapped

Hi,

jhack crashes if executed on machine without juju bootstrapped:

ubuntu@taurus-dev:~$ sudo snap install jhack --channel edge
jhack (edge) 0.3.19.1.1 from Pietro Pasotti (ppasotti) installed

ubuntu@taurus-dev:~$ jhack ffwd
Traceback (most recent call last):
  File "/snap/jhack/235/bin/jhack", line 8, in <module>
    sys.exit(main())
  File "/snap/jhack/235/lib/python3.8/site-packages/jhack/main.py", line 23, in main
    configure()
  File "/snap/jhack/235/lib/python3.8/site-packages/jhack/config.py", line 87, in configure
    test_file.write_text("kuckadoodle-foo")
  File "/usr/lib/python3.8/pathlib.py", line 1255, in write_text
    with self.open(mode='w', encoding=encoding, errors=errors) as f:
  File "/usr/lib/python3.8/pathlib.py", line 1222, in open
    return io.open(self, mode, buffering, encoding, errors, newline,
  File "/usr/lib/python3.8/pathlib.py", line 1078, in _opener
    return self._accessor.open(self, flags, mode)
FileNotFoundError: [Errno 2] No such file or directory: '/home/ubuntu/.local/share/juju/.__test_rw_jhack__.hacky'

ubuntu@taurus-dev:~$ snap list jhack,juju
Name   Version        Rev    Tracking     Publisher   Notes
jhack  0.3.19.1.1     235    latest/edge  ppasotti    -
juju   3.1.6-4a2434a  24210  3.1/edge     canonical✓  -

Sure, it is minor, but good to write reasonable error here, e.g. Juju not found, etc.

BTW, it is time to update stable track! ;-)

channels:
  latest/stable:    0.3.14     2023-03-13 (201) 85MB -
  latest/candidate: 0.3.14     2023-03-13 (201) 85MB -
  latest/beta:      0.3.18.3   2023-07-28 (225) 89MB -
  latest/edge:      0.3.19.1.1 2023-07-28 (235) 89MB -

`metadata.yaml` is not synced

Is jhack utils sync only syncing *.py files?

I've modified metadata.yaml but it is not synced:

$ jhack utils sync indico/0                                                                                                          
watching: 
	/home/ubuntu/repos/indico-operator/src/charm.py
	/home/ubuntu/repos/indico-operator/src/state.py
	/home/ubuntu/repos/indico-operator/lib/charms/grafana_k8s/v0/grafana_dashboard.py
	/home/ubuntu/repos/indico-operator/lib/charms/loki_k8s/v0/loki_push_api.py
	/home/ubuntu/repos/indico-operator/lib/charms/nginx_ingress_integrator/v0/nginx_route.py
	/home/ubuntu/repos/indico-operator/lib/charms/observability_libs/v0/juju_topology.py
	/home/ubuntu/repos/indico-operator/lib/charms/prometheus_k8s/v0/prometheus_scrape.py
	/home/ubuntu/repos/indico-operator/lib/charms/redis_k8s/v0/redis.py
Ctrl+C to interrupt

But the doc says it sync a folder:

$ jhack utils sync --help
                                                                                                                                             
 Usage: jhack utils sync [OPTIONS] TARGET                                                                                                    
                                                                                                                                             
 Syncs a local folder to a remote juju unit via juju scp.    

Version installed: 0.3.19.1.1

Unable to run `jhack tail` without `--watch`

Is it possible to run jhack tail without watching?

If --watch is not passed, it defaults to True. If --watch is passed, it is True.

jhack tail --watch=False fails with Option '--watch' does not take a value.

and jhack tail --no-watch fails with No such option: --no-watch Did you mean --watch?

`jhack fire` cannot access the agent socket

I tried firing an event and it failed:

$ jhack fire lxd-cloud-cell/77 cluster-relation-changed
ERROR:jhack.simulate_event:cmd juju ssh lxd-cloud-cell/77 /usr/bin/juju-exec -u lxd-cloud-cell/77 JUJU_DISPATCH_PATH=hooks/cluster-relation-changed JUJU_MODEL_NAME=test JUJU_UNIT_NAME=lxd-cloud-cell/77 JUJU_RELATION=cluster JUJU_RELATION_ID=29 ./dispatch terminated with 1
ERROR:jhack.simulate_event:stdout=b'\x1b[91mERROR\x1b[0m dialing juju run socket: dial unix /var/lib/juju/agents/unit-lxd-cloud-cell-77/run.socket: connect: permission denied\r\n'
ERROR:jhack.simulate_event:stderr=b'Connection to 10.184.166.67 closed.\r\n'
Fired cluster-relation-changed on lxd-cloud-cell/77.

The socket is only accessible by root:

$ juju ssh lxd-cloud-cell/77 ls -l /var/lib/juju/agents/unit-lxd-cloud-cell-77/run.socket
srwx------ 1 root root 0 Mar 10 14:49 /var/lib/juju/agents/unit-lxd-cloud-cell-77/run.socket
Connection to 10.184.166.67 closed.

And at least here, juju ssh uses ubuntu:

$ juju ssh lxd-cloud-cell/77 whoami
ubuntu
Connection to 10.184.166.67 closed.

Additional information:

$ snap list juju jhack
Name   Version  Rev    Tracking     Publisher   Notes
jhack  0.3.14   198    latest/edge  ppasotti    -
juju   3.1.0    22136  3.1/stable   canonical✓  -

"show-relation" doesn't seem to work on subordinate relations

I did a quick test of jhack locally on LXD, using an ubuntu unit and an nrpe subordinate.

My invocation of jhack was: jhack show-relation ubuntu:juju-info nrpe:general-info

This gives the following traceback:

Traceback (most recent call last):
  File "/snap/jhack/120/bin/jhack", line 8, in <module>
    sys.exit(main())
  File "/snap/jhack/120/lib/python3.8/site-packages/jhack/main.py", line 134, in main
    app()
  File "/snap/jhack/120/lib/python3.8/site-packages/typer/main.py", line 214, in __call__
    return get_command(self)(*args, **kwargs)
  File "/snap/jhack/120/lib/python3.8/site-packages/click/core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "/snap/jhack/120/lib/python3.8/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "/snap/jhack/120/lib/python3.8/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/snap/jhack/120/lib/python3.8/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/snap/jhack/120/lib/python3.8/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "/snap/jhack/120/lib/python3.8/site-packages/typer/main.py", line 500, in wrapper
    return callback(**use_params)  # type: ignore
  File "/snap/jhack/120/lib/python3.8/site-packages/jhack/utils/show_relation.py", line 572, in sync_show_relation
    return _sync_show_relation(
  File "/snap/jhack/120/lib/python3.8/site-packages/jhack/utils/show_relation.py", line 609, in _sync_show_relation
    table = asyncio.run(
  File "/usr/lib/python3.8/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/usr/lib/python3.8/asyncio/base_events.py", line 616, in run_until_complete
    return future.result()
  File "/snap/jhack/120/lib/python3.8/site-packages/jhack/utils/show_relation.py", line 459, in render_relation
    data = get_relation_data(
  File "/snap/jhack/120/lib/python3.8/site-packages/jhack/utils/show_relation.py", line 325, in get_relation_data
    provider_data = get_content(
  File "/snap/jhack/120/lib/python3.8/site-packages/jhack/utils/show_relation.py", line 238, in get_content
    other_unit_name = next(iter(status["applications"][other_app_name]["units"]))
KeyError: 'units'

I suspect the above issue is happening because juju status output for subordinate applications doesn't include a "units" dictionary; you'd have to go into the principal unit to find those subordinate units.

Reproduction steps:

  • Use a Juju 2.9-series controller. (Tested with Juju 2.9.38.)
  • On a new model:
    • juju deploy ubuntu
    • juju deploy nrpe
    • juju add-relation ubuntu nrpe
    • After deployed and settled, run jhack show-relation ubuntu:juju-info nrpe:general-info.

jhack replay emit warnings

Jhack replay issues some warnings when it finds whitespace in the value of the environment variables it attempts to call dispatch with. Those vars will be skipped.

Solution: find a way to escape the command in such a way that we can pass ENVVAR="string with spaces" without breaking jhack.

KeyError: 'getpwuid(): uid not found

Hello,

Trying to run any jhack command gives:

$ jhack --help
Traceback (most recent call last):
  File "/snap/jhack/201/bin/jhack", line 8, in <module>
    sys.exit(main())
  File "/snap/jhack/201/lib/python3.8/site-packages/jhack/main.py", line 19, in main
    from jhack.config import configure
  File "/snap/jhack/201/lib/python3.8/site-packages/jhack/config.py", line 10, in <module>
    USR = pwd.getpwuid(os.getuid())[0]
KeyError: 'getpwuid(): uid not found: 115271064'

jhack tail doesn't like subordinates

Afaict jhack tail parsing of juju status chokes on subordinates.

When running jhack tail against a model full of machine charms (OpenStack) I'm getting this traceback

Traceback (most recent call last):
  File "/home/peter/src/juju/jhack/jhack/main.py", line 108, in <module>
    main()
  File "/home/peter/src/juju/jhack/jhack/main.py", line 104, in main
    app()
  File "/home/peter/venv/devtools/lib/python3.10/site-packages/typer/main.py", line 214, in __call__
    return get_command(self)(*args, **kwargs)
  File "/home/peter/venv/devtools/lib/python3.10/site-packages/click/core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "/home/peter/venv/devtools/lib/python3.10/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "/home/peter/venv/devtools/lib/python3.10/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/home/peter/venv/devtools/lib/python3.10/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/home/peter/venv/devtools/lib/python3.10/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "/home/peter/venv/devtools/lib/python3.10/site-packages/typer/main.py", line 500, in wrapper
    return callback(**use_params)  # type: ignore
  File "/home/peter/src/juju/jhack/jhack/utils/tail_charms.py", line 964, in tail_events
    return _tail_events(
  File "/home/peter/src/juju/jhack/jhack/utils/tail_charms.py", line 1013, in _tail_events
    targets = parse_targets(targets)
  File "/home/peter/src/juju/jhack/jhack/utils/tail_charms.py", line 87, in parse_targets
    return get_all_units()
  File "/home/peter/src/juju/jhack/jhack/utils/tail_charms.py", line 80, in get_all_units
    target = Target.from_name(first_part)
  File "/home/peter/src/juju/jhack/jhack/utils/tail_charms.py", line 48, in from_name
    app, unit_ = name.split("/")
ValueError: not enough values to unpack (expected 2, got 1)

jhack tail throws `ERROR cannot load ssh client keys`

I am not sure if I am missing some configuration step, I have tried following this guide.

Observed behaviour

Running jhack tail throws the following error:

WARNING:jhack:JUJU_DATA is None
WARNING:jhack:setting JUJU_DATA to: /home/ubuntu/.local/share/juju/
ERROR cannot load ssh client keys: open /home/ubuntu/.local/share/juju/ssh: permission denied
No events caught.

Steps to reproduce

sudo snap install jhack --channel latest/edge
sudo snap set jhack juju=/snap/bin/juju jujudata=/home/USERNAME/.local/share/juju/ # my username is ubuntu
juju model-config logging-config="<root>=WARNING;unit=DEBUG"
juju deploy kubeflow --channel cfk-1.6/beta --trust # large bundle
jhack tail

Watching relations does not show new units

Using juju show-relation --watch on a relation does not show units added.

Workaround is to exit jhack and re-run the command.

As a reference, the command actually ran:

jhack show-relation --show-juju-keys barbican:secrets vault:secrets --watch

imatrix crashes on terminated application

Hi,

There is a well known Juju K8s issue which causes terminated state for some application as charm got killed by juju->pebble, Fix, WIP.

the command jhack imatrix view crashes if models has such an application (see status=terminated, workload=unknown for s3-integrator):

> juju status
Model     Controller  Cloud/Region        Version  SLA          Timestamp
welcome6  charm-dev   microk8s/localhost  2.9.38   unsupported  13:29:44+01:00

App                        Version                  Status      Scale  Charm                      Channel  Rev  Address         Exposed  Message
application                                         waiting         1  application                           0  10.152.183.247  no       installing agent
mysql-k8s                  8.0.31-0ubuntu0.22.04.1  active          1  mysql-k8s                  edge      39  10.152.183.203  no       
mysql-router-k8s           8.0.30                   active          1  mysql-router-k8s                      1  10.152.183.56   no       
s3-integrator                                       terminated    0/1  s3-integrator              edge       6  10.152.183.69   no       unit stopped by the cloud
tls-certificates-operator                           active          1  tls-certificates-operator  edge      22  10.152.183.161  no       

Unit                          Workload  Agent  Address      Ports  Message
application/0*                blocked   idle   10.1.84.122         Able to write using the read-only connection
mysql-k8s/0*                  active    idle   10.1.84.125         
mysql-router-k8s/0*           active    idle   10.1.84.65          
s3-integrator/0               unknown   lost   10.1.84.118         agent lost, see 'juju show-status-log s3-integrator/0'
tls-certificates-operator/0*  active    idle   10.1.84.127         
the full trace
ubuntu@taurus-dev:~/mysql-router-k8s-operator/tests/integration/application-charm$ jhack imatrix view
ERROR pod "s3-integrator-0" not found
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /snap/jhack/186/lib/python3.8/site-packages/jhack/helpers.py:224 in fetch_file                   │
│                                                                                                  │
│   221 │   model_arg = f" -m {model}" if model else ""                                            │
│   222 │   cmd = f"juju ssh{model_arg} {unit} cat /var/lib/juju/agents/unit-{unit_sanitized}/ch   │
│   223 │   try:                                                                                   │
│ ❱ 224 │   │   raw = check_output(cmd.split())                                                    │
│   225 │   except CalledProcessError as e:                                                        │
│   226 │   │   raise RuntimeError(                                                                │
│   227 │   │   │   f"Failed to fetch {remote_path} from {unit_sanitized}."                        │
│                                                                                                  │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │            cmd = 'juju ssh s3-integrator/0 cat                                               │ │
│ │                  /var/lib/juju/agents/unit-s3-integrator-0/charm/met'+10                     │ │
│ │     local_path = None                                                                        │ │
│ │          model = None                                                                        │ │
│ │      model_arg = ''                                                                          │ │
│ │    remote_path = 'metadata.yaml'                                                             │ │
│ │           unit = 's3-integrator/0'                                                           │ │
│ │ unit_sanitized = 's3-integrator-0'                                                           │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                                  │
│ /usr/lib/python3.8/subprocess.py:415 in check_output                                             │
│                                                                                                  │
│    412 │   │   │   empty = b''                                                                   │
│    413 │   │   kwargs['input'] = empty                                                           │
│    414 │                                                                                         │
│ ❱  415 │   return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,                      │
│    416 │   │   │      **kwargs).stdout                                                           │
│    417                                                                                           │
│    418                                                                                           │
│                                                                                                  │
│ ╭────────────────────────────────────── locals ───────────────────────────────────────╮          │
│ │    kwargs = {}                                                                      │          │
│ │ popenargs = (                                                                       │          │
│ │             │   [                                                                   │          │
│ │             │   │   'juju',                                                         │          │
│ │             │   │   'ssh',                                                          │          │
│ │             │   │   's3-integrator/0',                                              │          │
│ │             │   │   'cat',                                                          │          │
│ │             │   │   '/var/lib/juju/agents/unit-s3-integrator-0/charm/metadata.yaml' │          │
│ │             │   ],                                                                  │          │
│ │             )                                                                       │          │
│ │   timeout = None                                                                    │          │
│ ╰─────────────────────────────────────────────────────────────────────────────────────╯          │
│                                                                                                  │
│ /usr/lib/python3.8/subprocess.py:516 in run                                                      │
│                                                                                                  │
│    513 │   │   │   raise                                                                         │
│    514 │   │   retcode = process.poll()                                                          │
│    515 │   │   if check and retcode:                                                             │
│ ❱  516 │   │   │   raise CalledProcessError(retcode, process.args,                               │
│    517 │   │   │   │   │   │   │   │   │    output=stdout, stderr=stderr)                        │
│    518 │   return CompletedProcess(process.args, retcode, stdout, stderr)                        │
│    519                                                                                           │
│                                                                                                  │
│ ╭───────────────────────────────────────── locals ─────────────────────────────────────────╮     │
│ │ capture_output = False                                                                   │     │
│ │          check = True                                                                    │     │
│ │          input = None                                                                    │     │
│ │         kwargs = {'stdout': -1}                                                          │     │
│ │      popenargs = (                                                                       │     │
│ │                  │   [                                                                   │     │
│ │                  │   │   'juju',                                                         │     │
│ │                  │   │   'ssh',                                                          │     │
│ │                  │   │   's3-integrator/0',                                              │     │
│ │                  │   │   'cat',                                                          │     │
│ │                  │   │   '/var/lib/juju/agents/unit-s3-integrator-0/charm/metadata.yaml' │     │
│ │                  │   ],                                                                  │     │
│ │                  )                                                                       │     │
│ │        process =                              │     │
│ │        retcode = 1                                                                       │     │
│ │         stderr = None                                                                    │     │
│ │         stdout = b''                                                                     │     │
│ │        timeout = None                                                                    │     │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────╯     │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
CalledProcessError: Command '['juju', 'ssh', 's3-integrator/0', 'cat', '/var/lib/juju/agents/unit-s3-integrator-0/charm/metadata.yaml']' returned non-zero exit status 1.

The above exception was the direct cause of the following exception:

╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /snap/jhack/186/lib/python3.8/site-packages/jhack/utils/integrate.py:370 in show │
│ │
│ 367 │ color: Optional[str] = ColorOption, │
│ 368 ): │
│ 369 │ """Display the avaiable integrations between any number of juju applications in a ni │
│ ❱ 370 │ mtrx = IntegrationMatrix(apps=apps, model=model, color=color) │
│ 371 │ if watch: │
│ 372 │ │ mtrx.watch(refresh_rate=refresh_rate) │
│ 373 │ else: │
│ │
│ ╭─────── locals ────────╮ │
│ │ apps = None │ │
│ │ color = 'auto' │ │
│ │ model = None │ │
│ │ refresh_rate = None │ │
│ │ watch = None │ │
│ ╰───────────────────────╯ │
│ │
│ /snap/jhack/186/lib/python3.8/site-packages/jhack/utils/integrate.py:78 in init
│ │
│ 75 │ ): │
│ 76 │ │ self._model = model │
│ 77 │ │ self._color = color │
│ ❱ 78 │ │ self._endpoints = _gather_endpoints(model, apps) │
│ 79 │ │ self._apps = tuple(sorted(self._endpoints)) │
│ 80 │ │ │
│ 81 │ │ if apps: │
│ │
│ ╭────────────────────────────────── locals ──────────────────────────────────╮ │
│ │ apps = None │ │
│ │ color = 'auto' │ │
│ │ model = None │ │
│ │ self = <jhack.utils.integrate.IntegrationMatrix object at 0x7f597767a400> │ │
│ ╰────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /snap/jhack/186/lib/python3.8/site-packages/jhack/utils/integrate.py:54 in _gather_endpoints │
│ │
│ 51 │ │ │
│ 52 │ │ app_eps = {} │
│ 53 │ │ unit = next(iter(app["units"])) │
│ ❱ 54 │ │ metadata = fetch_file(unit, "metadata.yaml", model=model) │
│ 55 │ │ meta = yaml.safe_load(metadata) │
│ 56 │ │ │
│ 57 │ │ for role in ("requires", "provides"): │
│ │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │ all_apps = { │ │
│ │ │ 'application': { │ │
│ │ │ │ 'charm': 'local:focal/application-0', │ │
│ │ │ │ 'base': {'name': 'ubuntu', 'channel': '20.04'}, │ │
│ │ │ │ 'charm-origin': 'local', │ │
│ │ │ │ 'charm-name': 'application', │ │
│ │ │ │ 'charm-rev': 0, │ │
│ │ │ │ 'scale': 1, │ │
│ │ │ │ 'provider-id': '0440e2ff-2cbf-4826-8190-5f790ecc3b1e', │ │
│ │ │ │ 'address': '10.152.183.247', │ │
│ │ │ │ 'exposed': False, │ │
│ │ │ │ 'application-status': { │ │
│ │ │ │ │ 'current': 'waiting', │ │
│ │ │ │ │ 'message': 'installing agent', │ │
│ │ │ │ │ 'since': '27 Feb 2023 13:28:35+01:00' │ │
│ │ │ │ }, │ │
│ │ │ │ ... +3 │ │
│ │ │ }, │ │
│ │ │ 'mysql-k8s': { │ │
│ │ │ │ 'charm': 'mysql-k8s', │ │
│ │ │ │ 'base': {'name': 'kubernetes', 'channel': 'kubernetes'}, │ │
│ │ │ │ 'charm-origin': 'charmhub', │ │
│ │ │ │ 'charm-name': 'mysql-k8s', │ │
│ │ │ │ 'charm-rev': 39, │ │
│ │ │ │ 'charm-channel': 'edge', │ │
│ │ │ │ 'can-upgrade-to': 'ch:amd64/jammy/mysql-k8s-40', │ │
│ │ │ │ 'scale': 1, │ │
│ │ │ │ 'provider-id': 'e1edea21-6095-4786-a0fa-abb782e7d187', │ │
│ │ │ │ 'address': '10.152.183.203', │ │
│ │ │ │ ... +6 │ │
│ │ │ }, │ │
│ │ │ 'mysql-router-k8s': { │ │
│ │ │ │ 'charm': 'local:jammy/mysql-router-k8s-1', │ │
│ │ │ │ 'base': {'name': 'kubernetes', 'channel': 'kubernetes'}, │ │
│ │ │ │ 'charm-origin': 'local', │ │
│ │ │ │ 'charm-name': 'mysql-router-k8s', │ │
│ │ │ │ 'charm-rev': 1, │ │
│ │ │ │ 'scale': 1, │ │
│ │ │ │ 'provider-id': 'baa5d805-1ff5-4f74-b68d-5fc188f13c8b', │ │
│ │ │ │ 'address': '10.152.183.56', │ │
│ │ │ │ 'exposed': False, │ │
│ │ │ │ 'application-status': { │ │
│ │ │ │ │ 'current': 'active', │ │
│ │ │ │ │ 'since': '27 Feb 2023 13:28:13+01:00' │ │
│ │ │ │ }, │ │
│ │ │ │ ... +4 │ │
│ │ │ }, │ │
│ │ │ 's3-integrator': { │ │
│ │ │ │ 'charm': 's3-integrator', │ │
│ │ │ │ 'base': {'name': 'ubuntu', 'channel': '22.04'}, │ │
│ │ │ │ 'charm-origin': 'charmhub', │ │
│ │ │ │ 'charm-name': 's3-integrator', │ │
│ │ │ │ 'charm-rev': 6, │ │
│ │ │ │ 'charm-channel': 'edge', │ │
│ │ │ │ 'scale': 1, │ │
│ │ │ │ 'provider-id': '595335ff-8241-429f-89c6-d1747296ecc9', │ │
│ │ │ │ 'address': '10.152.183.69', │ │
│ │ │ │ 'exposed': False, │ │
│ │ │ │ ... +5 │ │
│ │ │ }, │ │
│ │ │ 'tls-certificates-operator': { │ │
│ │ │ │ 'charm': 'tls-certificates-operator', │ │
│ │ │ │ 'base': {'name': 'ubuntu', 'channel': '22.04'}, │ │
│ │ │ │ 'charm-origin': 'charmhub', │ │
│ │ │ │ 'charm-name': 'tls-certificates-operator', │ │
│ │ │ │ 'charm-rev': 22, │ │
│ │ │ │ 'charm-channel': 'edge', │ │
│ │ │ │ 'scale': 1, │ │
│ │ │ │ 'provider-id': '64a7405c-e4c3-4709-b9b6-7c18eeb3b06b', │ │
│ │ │ │ 'address': '10.152.183.161', │ │
│ │ │ │ 'exposed': False, │ │
│ │ │ │ ... +4 │ │
│ │ │ } │ │
│ │ } │ │
│ │ app = { │ │
│ │ │ 'charm': 's3-integrator', │ │
│ │ │ 'base': {'name': 'ubuntu', 'channel': '22.04'}, │ │
│ │ │ 'charm-origin': 'charmhub', │ │
│ │ │ 'charm-name': 's3-integrator', │ │
│ │ │ 'charm-rev': 6, │ │
│ │ │ 'charm-channel': 'edge', │ │
│ │ │ 'scale': 1, │ │
│ │ │ 'provider-id': '595335ff-8241-429f-89c6-d1747296ecc9', │ │
│ │ │ 'address': '10.152.183.69', │ │
│ │ │ 'exposed': False, │ │
│ │ │ ... +5 │ │
│ │ } │ │
│ │ app_eps = {} │ │
│ │ app_name = 's3-integrator' │ │
│ │ apps = None │ │
│ │ eps = { │ │
│ │ │ 'application': { │ │
│ │ │ │ 'requires': {'database': ('mysql_client', ['mysql-router-k8s'])}, │ │
│ │ │ │ 'provides': {} │ │
│ │ │ }, │ │
│ │ │ 'mysql-k8s': { │ │
│ │ │ │ 'requires': { │ │
│ │ │ │ │ 'certificates': ('tls-certificates', ['tls-certificates-operator']), │ │
│ │ │ │ │ 's3-parameters': ('s3', []) │ │
│ │ │ │ }, │ │
│ │ │ │ 'provides': { │ │
│ │ │ │ │ 'mysql': ('mysql', []), │ │
│ │ │ │ │ 'database': ('mysql_client', ['mysql-router-k8s']), │ │
│ │ │ │ │ 'osm-mysql': ('mysql', []) │ │
│ │ │ │ } │ │
│ │ │ }, │ │
│ │ │ 'mysql-router-k8s': { │ │
│ │ │ │ 'requires': { │ │
│ │ │ │ │ 'backend-database': ('mysql_client', ['mysql-k8s']), │ │
│ │ │ │ │ 'certificates': ('tls-certificates', ['tls-certificates-operator']) │ │
│ │ │ │ }, │ │
│ │ │ │ 'provides': {'database': ('mysql_client', ['application'])} │ │
│ │ │ } │ │
│ │ } │ │
│ │ meta = { │ │
│ │ │ 'name': 'mysql-router-k8s', │ │
│ │ │ 'display-name': 'MySQL Router', │ │
│ │ │ 'maintainers': [ │ │
│ │ │ │ 'Paulo Machado [email protected]', │ │
│ │ │ │ 'Shayan Patel [email protected]' │ │
│ │ │ ], │ │
│ │ │ 'description': 'K8S charmed operator for mysql-router.\n', │ │
│ │ │ 'summary': 'Charmed operator for mysql-router.\nEnables effective access to │ │
│ │ group replicated '+39, │ │
│ │ │ 'containers': {'mysql-router': {'resource': 'mysql-router-image'}}, │ │
│ │ │ 'provides': {'database': {'interface': 'mysql_client'}}, │ │
│ │ │ 'requires': { │ │
│ │ │ │ 'backend-database': {'interface': 'mysql_client', 'limit': 1}, │ │
│ │ │ │ 'certificates': {'interface': 'tls-certificates', 'limit': 1} │ │
│ │ │ }, │ │
│ │ │ 'peers': {'mysql-router-peers': {'interface': 'mysql-router-peers'}}, │ │
│ │ │ 'resources': { │ │
│ │ │ │ 'mysql-router-image': { │ │
│ │ │ │ │ 'type': 'oci-image', │ │
│ │ │ │ │ 'description': 'OCI image for mysql-router', │ │
│ │ │ │ │ 'upstream-source': 'dataplatformoci/mysql-router:latest' │ │
│ │ │ │ } │ │
│ │ │ }, │ │
│ │ │ ... +1 │ │
│ │ } │ │
│ │ metadata = '# Copyright 2022 Canonical Ltd.\r\n# See LICENSE file for licensing │ │
│ │ details.\r\nname'+910 │ │
│ │ model = None │ │
│ │ remotes = <function _gather_endpoints..remotes at 0x7f5977664d30> │ │
│ │ role = 'provides' │ │
│ │ role_eps = {'database': ('mysql_client', ['application'])} │ │
│ │ status = { │ │
│ │ │ 'model': { │ │
│ │ │ │ 'name': 'welcome6', │ │
│ │ │ │ 'type': 'caas', │ │
│ │ │ │ 'controller': 'charm-dev', │ │
│ │ │ │ 'cloud': 'microk8s', │ │
│ │ │ │ 'region': 'localhost', │ │
│ │ │ │ 'version': '2.9.38', │ │
│ │ │ │ 'model-status': { │ │
│ │ │ │ │ 'current': 'available', │ │
│ │ │ │ │ 'since': '24 Feb 2023 11:51:18+01:00' │ │
│ │ │ │ }, │ │
│ │ │ │ 'sla': 'unsupported' │ │
│ │ │ }, │ │
│ │ │ 'machines': {}, │ │
│ │ │ 'applications': { │ │
│ │ │ │ 'application': { │ │
│ │ │ │ │ 'charm': 'local:focal/application-0', │ │
│ │ │ │ │ 'base': {'name': 'ubuntu', 'channel': '20.04'}, │ │
│ │ │ │ │ 'charm-origin': 'local', │ │
│ │ │ │ │ 'charm-name': 'application', │ │
│ │ │ │ │ 'charm-rev': 0, │ │
│ │ │ │ │ 'scale': 1, │ │
│ │ │ │ │ 'provider-id': '0440e2ff-2cbf-4826-8190-5f790ecc3b1e', │ │
│ │ │ │ │ 'address': '10.152.183.247', │ │
│ │ │ │ │ 'exposed': False, │ │
│ │ │ │ │ 'application-status': { │ │
│ │ │ │ │ │ 'current': 'waiting', │ │
│ │ │ │ │ │ 'message': 'installing agent', │ │
│ │ │ │ │ │ 'since': '27 Feb 2023 13:28:35+01:00' │ │
│ │ │ │ │ }, │ │
│ │ │ │ │ ... +3 │ │
│ │ │ │ }, │ │
│ │ │ │ 'mysql-k8s': { │ │
│ │ │ │ │ 'charm': 'mysql-k8s', │ │
│ │ │ │ │ 'base': {'name': 'kubernetes', 'channel': 'kubernetes'}, │ │
│ │ │ │ │ 'charm-origin': 'charmhub', │ │
│ │ │ │ │ 'charm-name': 'mysql-k8s', │ │
│ │ │ │ │ 'charm-rev': 39, │ │
│ │ │ │ │ 'charm-channel': 'edge', │ │
│ │ │ │ │ 'can-upgrade-to': 'ch:amd64/jammy/mysql-k8s-40', │ │
│ │ │ │ │ 'scale': 1, │ │
│ │ │ │ │ 'provider-id': 'e1edea21-6095-4786-a0fa-abb782e7d187', │ │
│ │ │ │ │ 'address': '10.152.183.203', │ │
│ │ │ │ │ ... +6 │ │
│ │ │ │ }, │ │
│ │ │ │ 'mysql-router-k8s': { │ │
│ │ │ │ │ 'charm': 'local:jammy/mysql-router-k8s-1', │ │
│ │ │ │ │ 'base': {'name': 'kubernetes', 'channel': 'kubernetes'}, │ │
│ │ │ │ │ 'charm-origin': 'local', │ │
│ │ │ │ │ 'charm-name': 'mysql-router-k8s', │ │
│ │ │ │ │ 'charm-rev': 1, │ │
│ │ │ │ │ 'scale': 1, │ │
│ │ │ │ │ 'provider-id': 'baa5d805-1ff5-4f74-b68d-5fc188f13c8b', │ │
│ │ │ │ │ 'address': '10.152.183.56', │ │
│ │ │ │ │ 'exposed': False, │ │
│ │ │ │ │ 'application-status': { │ │
│ │ │ │ │ │ 'current': 'active', │ │
│ │ │ │ │ │ 'since': '27 Feb 2023 13:28:13+01:00' │ │
│ │ │ │ │ }, │ │
│ │ │ │ │ ... +4 │ │
│ │ │ │ }, │ │
│ │ │ │ 's3-integrator': { │ │
│ │ │ │ │ 'charm': 's3-integrator', │ │
│ │ │ │ │ 'base': {'name': 'ubuntu', 'channel': '22.04'}, │ │
│ │ │ │ │ 'charm-origin': 'charmhub', │ │
│ │ │ │ │ 'charm-name': 's3-integrator', │ │
│ │ │ │ │ 'charm-rev': 6, │ │
│ │ │ │ │ 'charm-channel': 'edge', │ │
│ │ │ │ │ 'scale': 1, │ │
│ │ │ │ │ 'provider-id': '595335ff-8241-429f-89c6-d1747296ecc9', │ │
│ │ │ │ │ 'address': '10.152.183.69', │ │
│ │ │ │ │ 'exposed': False, │ │
│ │ │ │ │ ... +5 │ │
│ │ │ │ }, │ │
│ │ │ │ 'tls-certificates-operator': { │ │
│ │ │ │ │ 'charm': 'tls-certificates-operator', │ │
│ │ │ │ │ 'base': {'name': 'ubuntu', 'channel': '22.04'}, │ │
│ │ │ │ │ 'charm-origin': 'charmhub', │ │
│ │ │ │ │ 'charm-name': 'tls-certificates-operator', │ │
│ │ │ │ │ 'charm-rev': 22, │ │
│ │ │ │ │ 'charm-channel': 'edge', │ │
│ │ │ │ │ 'scale': 1, │ │
│ │ │ │ │ 'provider-id': '64a7405c-e4c3-4709-b9b6-7c18eeb3b06b', │ │
│ │ │ │ │ 'address': '10.152.183.161', │ │
│ │ │ │ │ 'exposed': False, │ │
│ │ │ │ │ ... +4 │ │
│ │ │ │ } │ │
│ │ │ }, │ │
│ │ │ 'storage': { │ │
│ │ │ │ 'storage': { │ │
│ │ │ │ │ 'database/1': { │ │
│ │ │ │ │ │ 'kind': 'filesystem', │ │
│ │ │ │ │ │ 'life': 'alive', │ │
│ │ │ │ │ │ 'status': { │ │
│ │ │ │ │ │ │ 'current': 'attached', │ │
│ │ │ │ │ │ │ 'since': '24 Feb 2023 17:59:27+01:00' │ │
│ │ │ │ │ │ }, │ │
│ │ │ │ │ │ 'persistent': False, │ │
│ │ │ │ │ │ 'attachments': {'units': {'mysql-k8s/0': {'life': 'alive'}}} │ │
│ │ │ │ │ }, │ │
│ │ │ │ │ 'pgdata/0': { │ │
│ │ │ │ │ │ 'kind': 'filesystem', │ │
│ │ │ │ │ │ 'life': 'alive', │ │
│ │ │ │ │ │ 'status': { │ │
│ │ │ │ │ │ │ 'current': 'detached', │ │
│ │ │ │ │ │ │ 'since': '27 Feb 2023 13:20:33+01:00' │ │
│ │ │ │ │ │ }, │ │
│ │ │ │ │ │ 'persistent': False │ │
│ │ │ │ │ } │ │
│ │ │ │ }, │ │
│ │ │ │ 'filesystems': { │ │
│ │ │ │ │ '0': { │ │
│ │ │ │ │ │ 'provider-id': 'c2e37220-1122-4a68-9f7e-c63c03e52b70', │ │
│ │ │ │ │ │ 'volume': '0', │ │
│ │ │ │ │ │ 'storage': 'pgdata/0', │ │
│ │ │ │ │ │ 'Attachments': None, │ │
│ │ │ │ │ │ 'pool': 'kubernetes', │ │
│ │ │ │ │ │ 'size': 1024, │ │
│ │ │ │ │ │ 'life': 'alive', │ │
│ │ │ │ │ │ 'status': { │ │
│ │ │ │ │ │ │ 'current': 'detached', │ │
│ │ │ │ │ │ │ 'since': '27 Feb 2023 13:20:33+01:00' │ │
│ │ │ │ │ │ } │ │
│ │ │ │ │ }, │ │
│ │ │ │ │ '1': { │ │
│ │ │ │ │ │ 'provider-id': '58b1f1a2-3231-4e08-8cc8-99974845721b', │ │
│ │ │ │ │ │ 'volume': '1', │ │
│ │ │ │ │ │ 'storage': 'database/1', │ │
│ │ │ │ │ │ 'Attachments': { │ │
│ │ │ │ │ │ │ 'containers': { │ │
│ │ │ │ │ │ │ │ 'mysql-k8s/0': { │ │
│ │ │ │ │ │ │ │ │ 'mount-point': '/var/lib/juju/storage/database/0', │ │
│ │ │ │ │ │ │ │ │ 'read-only': False, │ │
│ │ │ │ │ │ │ │ │ 'life': 'alive' │ │
│ │ │ │ │ │ │ │ } │ │
│ │ │ │ │ │ │ }, │ │
│ │ │ │ │ │ │ 'units': {'mysql-k8s/0': {'life': 'alive'}} │ │
│ │ │ │ │ │ }, │ │
│ │ │ │ │ │ 'pool': 'kubernetes', │ │
│ │ │ │ │ │ 'size': 1024, │ │
│ │ │ │ │ │ 'life': 'alive', │ │
│ │ │ │ │ │ 'status': { │ │
│ │ │ │ │ │ │ 'current': 'attached', │ │
│ │ │ │ │ │ │ 'since': '24 Feb 2023 17:59:27+01:00' │ │
│ │ │ │ │ │ } │ │
│ │ │ │ │ } │ │
│ │ │ │ }, │ │
│ │ │ │ 'volumes': { │ │
│ │ │ │ │ '0': { │ │
│ │ │ │ │ │ 'provider-id': 'pvc-c2e37220-1122-4a68-9f7e-c63c03e52b70', │ │
│ │ │ │ │ │ 'storage': 'pgdata/0', │ │
│ │ │ │ │ │ 'pool': 'kubernetes', │ │
│ │ │ │ │ │ 'size': 1024, │ │
│ │ │ │ │ │ 'persistent': False, │ │
│ │ │ │ │ │ 'life': 'alive', │ │
│ │ │ │ │ │ 'status': { │ │
│ │ │ │ │ │ │ 'current': 'detached', │ │
│ │ │ │ │ │ │ 'since': '27 Feb 2023 13:20:33+01:00' │ │
│ │ │ │ │ │ } │ │
│ │ │ │ │ }, │ │
│ │ │ │ │ '1': { │ │
│ │ │ │ │ │ 'provider-id': 'pvc-58b1f1a2-3231-4e08-8cc8-99974845721b', │ │
│ │ │ │ │ │ 'storage': 'database/1', │ │
│ │ │ │ │ │ 'attachments': { │ │
│ │ │ │ │ │ │ 'containers': { │ │
│ │ │ │ │ │ │ │ 'mysql-k8s/0': {'read-only': False, 'life': 'alive'} │ │
│ │ │ │ │ │ │ }, │ │
│ │ │ │ │ │ │ 'units': {'mysql-k8s/0': {'life': 'alive'}} │ │
│ │ │ │ │ │ }, │ │
│ │ │ │ │ │ 'pool': 'kubernetes', │ │
│ │ │ │ │ │ 'size': 1024, │ │
│ │ │ │ │ │ 'persistent': False, │ │
│ │ │ │ │ │ 'life': 'alive', │ │
│ │ │ │ │ │ 'status': { │ │
│ │ │ │ │ │ │ 'current': 'attached', │ │
│ │ │ │ │ │ │ 'since': '24 Feb 2023 11:53:27+01:00' │ │
│ │ │ │ │ │ } │ │
│ │ │ │ │ } │ │
│ │ │ │ } │ │
│ │ │ }, │ │
│ │ │ 'controller': {'timestamp': '13:29:46+01:00'} │ │
│ │ } │ │
│ │ unit = 's3-integrator/0' │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /snap/jhack/186/lib/python3.8/site-packages/jhack/helpers.py:226 in fetch_file │
│ │
│ 223 │ try: │
│ 224 │ │ raw = check_output(cmd.split()) │
│ 225 │ except CalledProcessError as e: │
│ ❱ 226 │ │ raise RuntimeError( │
│ 227 │ │ │ f"Failed to fetch {remote_path} from {unit_sanitized}." │
│ 228 │ │ ) from e │
│ 229 │
│ │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │ cmd = 'juju ssh s3-integrator/0 cat │ │
│ │ /var/lib/juju/agents/unit-s3-integrator-0/charm/met'+10 │ │
│ │ local_path = None │ │
│ │ model = None │ │
│ │ model_arg = '' │ │
│ │ remote_path = 'metadata.yaml' │ │
│ │ unit = 's3-integrator/0' │ │
│ │ unit_sanitized = 's3-integrator-0' │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
RuntimeError: Failed to fetch metadata.yaml from s3-integrator-0.

Fresh build from source expects config.toml

A fresh install with git clone, pip install -r requirements then python3 main.py gives an error that the config file cannot be found

Traceback (most recent call last):
  File "/home/aflynn50/Canonical/jhack/jhack/main.py", line 43, in <module>
    from jhack.utils.nuke import nuke
  File "/home/aflynn50/Canonical/jhack/jhack/utils/nuke.py", line 22, in <module>
    ASK_FOR_CONFIRMATION = CONFIG["nuke"]["ask_for_confirmation"]
  File "/home/aflynn50/Canonical/jhack/jhack/conf/conf.py", line 32, in __getitem__
    return self.data[item]
  File "/home/aflynn50/Canonical/jhack/jhack/conf/conf.py", line 25, in data
    self._load()
  File "/home/aflynn50/Canonical/jhack/jhack/conf/conf.py", line 20, in _load
    self._data = toml.load(self._path.open())
  File "/usr/lib/python3.10/pathlib.py", line 1119, in open
    return self._accessor.open(self, mode, buffering, encoding, errors,
FileNotFoundError: [Errno 2] No such file or directory: '/home/aflynn50/.jhack_config.toml'

When this file is created then we get another error

Traceback (most recent call last):
  File "/home/aflynn50/Canonical/jhack/jhack/main.py", line 43, in <module>
    from jhack.utils.nuke import nuke
  File "/home/aflynn50/Canonical/jhack/jhack/utils/nuke.py", line 22, in <module>
    ASK_FOR_CONFIRMATION = CONFIG["nuke"]["ask_for_confirmation"]
  File "/home/aflynn50/Canonical/jhack/jhack/conf/conf.py", line 32, in __getitem__
    return self.data[item]
KeyError: 'nuke'

Which can apparently be fixed by putting echo "nuke.ask_for_confirmation = false" > ~/.jhack/config.toml

Permission denied to load pubkey

Using the snap I got permission denied for operation that needs ssh access to the instances. Adding this plug to snapcraft and run sudo snap connect jhack:ssh-read snapd fixed it. Why do I have this problem and not others? Because I run LXC? Anyway, I don't know much about Snap and if this is a good solution or not but at least I can use this amazing tool now 😃

$ jhack imatrix view
load pubkey "/home/joakim/.ssh/id_rsa": Permission denied
Load key "/home/joakim/.ssh/id_rsa": Permission denied
[email protected]: Permission denied (publickey).
Traceback (most recent call last):
  File "/snap/jhack/x1/lib/python3.8/site-packages/jhack/helpers.py", line 226, in fetch_file
    raw = check_output(cmd.split())
  File "/usr/lib/python3.8/subprocess.py", line 415, in check_output
    return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
  File "/usr/lib/python3.8/subprocess.py", line 516, in run
    raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command '['juju', 'ssh', 'ubuntu/0', 'cat', '/var/lib/juju/agents/unit-ubuntu-0/charm/metadata.yaml']' returned non-zero exit status 255.

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/snap/jhack/x1/bin/jhack", line 8, in <module>
    sys.exit(main())
  File "/snap/jhack/x1/lib/python3.8/site-packages/jhack/main.py", line 152, in main
    app()
  File "/snap/jhack/x1/lib/python3.8/site-packages/typer/main.py", line 214, in __call__
    return get_command(self)(*args, **kwargs)
  File "/snap/jhack/x1/lib/python3.8/site-packages/click/core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "/snap/jhack/x1/lib/python3.8/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "/snap/jhack/x1/lib/python3.8/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/snap/jhack/x1/lib/python3.8/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/snap/jhack/x1/lib/python3.8/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/snap/jhack/x1/lib/python3.8/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "/snap/jhack/x1/lib/python3.8/site-packages/typer/main.py", line 500, in wrapper
    return callback(**use_params)  # type: ignore
  File "/snap/jhack/x1/lib/python3.8/site-packages/jhack/utils/integrate.py", line 366, in show
    mtrx = IntegrationMatrix(apps=apps, model=model, color=color)
  File "/snap/jhack/x1/lib/python3.8/site-packages/jhack/utils/integrate.py", line 78, in __init__
    self._endpoints = _gather_endpoints(model, apps)
  File "/snap/jhack/x1/lib/python3.8/site-packages/jhack/utils/integrate.py", line 54, in _gather_endpoints
    metadata = fetch_file(unit, "metadata.yaml", model=model)
  File "/snap/jhack/x1/lib/python3.8/site-packages/jhack/helpers.py", line 228, in fetch_file
    raise RuntimeError(
RuntimeError: Failed to fetch metadata.yaml from ubuntu-0.

`jhack utils show-relation` shows an Exception

In a lxd controller I have 2 charms running:

╭─ubuntu@charm-dev-juju-29 ~ 
╰─$ juju status --color --relations                           
Model         Controller  Cloud/Region         Version  SLA          Timestamp
applications  lxd         localhost/localhost  2.9.38   unsupported  18:43:17-03:00

SAAS                             Status  Store                URL
loki-logging                     active  charm-dev-batteries  admin/cos.loki-logging
prometheus-receive-remote-write  active  charm-dev-batteries  admin/cos.prometheus-receive-remote-write

App            Version  Status  Scale  Charm          Channel  Rev  Exposed  Message
grafana-agent           active      1  grafana-agent             0  no       
zookeeper               active      1  zookeeper                 0  no       

Unit                Workload  Agent  Machine  Public address  Ports  Message
zookeeper/0*        active    idle   0        10.77.61.148           
  grafana-agent/0*  active    idle            10.77.61.148           

Machine  State    Address       Inst id        Series  AZ  Message
0        started  10.77.61.148  juju-a8713e-0  jammy       Running

Offer          Application    Charm          Rev  Connected  Endpoint                     Interface          Role
grafana-agent  grafana-agent  grafana-agent  0    0/0        grafana-dashboards-provider  grafana_dashboard  provider

Relation provider                                     Requirer                         Interface                Type         Message
loki-logging:logging                                  grafana-agent:logging-consumer   loki_push_api            regular      
prometheus-receive-remote-write:receive-remote-write  grafana-agent:send-remote-write  prometheus_remote_write  regular      
zookeeper:cluster                                     zookeeper:cluster                cluster                  peer         
zookeeper:cos-agent                                   grafana-agent:cos-agent          cos_agent                subordinate  
zookeeper:restart                                     zookeeper:restart                rolling_op               peer  

grafana-agent charm is a subordinate charm.

And when I execute:

jhack utils show-relation zookeeper:cos-agent grafana-agent

I obtain this error:

╭─ubuntu@charm-dev-juju-29 ~/repos/zookeeper-operator ‹cos-machine●› 
╰─$ jhack utils show-relation zookeeper:cos-agent grafana-agent                                                                          1 ↵
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /snap/jhack/201/lib/python3.8/site-packages/jhack/utils/show_relation.py:249 in get_content      │
│                                                                                                  │
│   246 │   """Get the content of the databag of `obj`, as seen from `other_obj`."""               │
│   247 │   try:                                                                                   │
│   248 │   │   this_url, this_endpoint = obj.split(":")                                           │
│ ❱ 249 │   │   other_url, other_endpoint = other_obj.split(":")                                   │
│   250 │   except ValueError:                                                                     │
│   251 │   │   raise ValueError(                                                                  │
│   252 │   │   │   "Relation endpoints need to be formatted as 'app_name:endpoint_name', "        │
│                                                                                                  │
│ ╭─────────────────────────── locals ────────────────────────────╮                                │
│ │ include_default_juju_keys = False                             │                                │
│ │                     model = None                              │                                │
│ │                       obj = 'zookeeper:cos-agent'             │                                │
│ │                 other_obj = 'grafana-agent'                   │                                │
│ │             relation_type = <RelationType.regular: 'regular'> │                                │
│ │             this_endpoint = 'cos-agent'                       │                                │
│ │                  this_url = 'zookeeper'                       │                                │
│ ╰───────────────────────────────────────────────────────────────╯                                │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
ValueError: not enough values to unpack (expected 2, got 1)

During handling of the above exception, another exception occurred:

╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /snap/jhack/201/lib/python3.8/site-packages/jhack/utils/show_relation.py:652 in                  │
│ sync_show_relation                                                                               │
│                                                                                                  │
│   649 │                                                                                          │
│   650 │   $ jhack utils show-relation my_app:relation_name other_app:other_name                  │
│   651 │   """
│ ❱ 652 │   return _sync_show_relation(                                                            │
│   653 │   │   endpoint1=endpoint1,                                                               │
│   654 │   │   endpoint2=endpoint2,                                                               │
│   655 │   │   n=n,                                                                               │
│                                                                                                  │
│ ╭────────────────── locals ───────────────────╮                                                  │
│ │               color = 'auto'                │                                                  │
│ │           endpoint1 = 'zookeeper:cos-agent' │                                                  │
│ │           endpoint2 = 'grafana-agent'       │                                                  │
│ │ hide_empty_databags = False                 │                                                  │
│ │               model = None                  │                                                  │
│ │                   n = None                  │                                                  │
│ │      show_juju_keys = False                 │                                                  │
│ │               watch = False                 │                                                  │
│ ╰─────────────────────────────────────────────╯                                                  │
│                                                                                                  │
│ /snap/jhack/201/lib/python3.8/site-packages/jhack/utils/show_relation.py:689 in                  │
│ _sync_show_relation                                                                              │
│                                                                                                  │
│   686 │   while True:                                                                            │
│   687 │   │   start = time.time()                                                                │
│   688 │   │                                                                                      │
│ ❱ 689 │   │   table = asyncio.run(                                                               │
│   690 │   │   │   render_relation(                                                               │
│   691 │   │   │   │   endpoint1,                                                                 │
│   692 │   │   │   │   endpoint2,                                                                 │
│                                                                                                  │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │               color = 'auto'                                                                 │ │
│ │             Console = <class 'rich.console.Console'>                                         │ │
│ │             console = <console width=141 ColorSystem.EIGHT_BIT>                              │ │
│ │           endpoint1 = 'zookeeper:cos-agent'                                                  │ │
│ │           endpoint2 = 'grafana-agent'                                                        │ │
│ │ hide_empty_databags = False                                                                  │ │
│ │               model = None                                                                   │ │
│ │                   n = None                                                                   │ │
│ │                rich = <module 'rich' from                                                    │ │
│ │                       '/snap/jhack/201/lib/python3.8/site-packages/rich/__init__.py'>        │ │
│ │      show_juju_keys = False                                                                  │ │
│ │               start = 1678830314.8960829                                                     │ │
│ │               watch = False                                                                  │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                                  │
│ /usr/lib/python3.8/asyncio/runners.py:44 in run                                                  │
│                                                                                                  │
│   41 │   │   events.set_event_loop(loop)                                                         │
│   42 │   │   if debug is not None:                                                               │
│   43 │   │   │   loop.set_debug(debug)                                                           │
│ ❱ 44 │   │   return loop.run_until_complete(main)                                                │
│   45 │   finally:                                                                                │
│   46 │   │   try:                                                                                │
│   47 │   │   │   _cancel_all_tasks(loop)                                                         │
│                                                                                                  │
│ ╭──────────────────────────────── locals ────────────────────────────────╮                       │
│ │ debug = None                                                           │                       │
│ │  loop = <_UnixSelectorEventLoop running=False closed=True debug=False> │                       │
│ │  main = <coroutine object render_relation at 0x7ff5cf652f40>           │                       │
│ ╰────────────────────────────────────────────────────────────────────────╯                       │
│                                                                                                  │
│ /usr/lib/python3.8/asyncio/base_events.py:616 in run_until_complete                              │
│                                                                                                  │
│    613 │   │   if not future.done():                                                             │
│    614 │   │   │   raise RuntimeError('Event loop stopped before Future completed.')             │
│    615 │   │                                                                                     │
│ ❱  616 │   │   return future.result()                                                            │
│    617 │                                                                                         │
│    618 │   def stop(self):                                                                       │
│    619 │   │   """Stop running the event loop.                                                   │
│                                                                                                  │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │   future = <Task finished name='Task-1' coro=<render_relation() done, defined at             │ │
│ │            /snap/jhack/201/lib/python3.8/site-packages/jhack/utils/show_relation.py:474>     │ │
│ │            exception=ValueError("Relation endpoints need to be formatted as                  │ │
│ │            'app_name:endpoint_name', e.g. 'traefik:ingress'. Not: 'zookeeper:cos-agent',     │ │
│ │            'grafana-agent'")>                                                                │ │
│ │ new_task = True                                                                              │ │
│ │     self = <_UnixSelectorEventLoop running=False closed=True debug=False>                    │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                                  │
│ /snap/jhack/201/lib/python3.8/site-packages/jhack/utils/show_relation.py:534 in render_relation  │
│                                                                                                  │
│   531 │   │   if not (endpoint1 and endpoint2):                                                  │
│   532 │   │   │   raise RuntimeError("invalid usage: provide two endpoints.")                    │
│   533 │   │                                                                                      │
│ ❱ 534 │   │   data = get_relation_data(                                                          │
│   535 │   │   │   provider_endpoint=endpoint1,                                                   │
│   536 │   │   │   requirer_endpoint=endpoint2,                                                   │
│   537 │   │   │   include_default_juju_keys=include_default_juju_keys,                           │
│                                                                                                  │
│ ╭───────────────────── locals ──────────────────────╮                                            │
│ │                 endpoint1 = 'zookeeper:cos-agent' │                                            │
│ │                 endpoint2 = 'grafana-agent'       │                                            │
│ │       hide_empty_databags = False                 │                                            │
│ │ include_default_juju_keys = False                 │                                            │
│ │                     model = None                  │                                            │
│ │                         n = None                  │                                            │
│ │             relation_type = None                  │                                            │
│ ╰───────────────────────────────────────────────────╯                                            │
│                                                                                                  │
│ /snap/jhack/201/lib/python3.8/site-packages/jhack/utils/show_relation.py:399 in                  │
│ get_relation_data                                                                                │
│                                                                                                  │
│   396 │                                                                                          │
│   397 │   >>> get_relation_data('prometheus/0:ingress', 'traefik/1:ingress-per-unit')            │
│   398 │   """
│ ❱ 399 │   provider_data = get_content(                                                           │
│   400 │   │   provider_endpoint, requirer_endpoint, include_default_juju_keys, model=model       │
│   401 │   )                                                                                      │
│   402 │   requirer_data = get_content(                                                           │
│                                                                                                  │
│ ╭───────────────────── locals ──────────────────────╮                                            │
│ │ include_default_juju_keys = False                 │                                            │
│ │                     model = None                  │                                            │
│ │         provider_endpoint = 'zookeeper:cos-agent' │                                            │
│ │         requirer_endpoint = 'grafana-agent'       │                                            │
│ ╰───────────────────────────────────────────────────╯                                            │
│                                                                                                  │
│ /snap/jhack/201/lib/python3.8/site-packages/jhack/utils/show_relation.py:251 in get_content      │
│                                                                                                  │
│   248 │   │   this_url, this_endpoint = obj.split(":")                                           │
│   249 │   │   other_url, other_endpoint = other_obj.split(":")                                   │
│   250 │   except ValueError:                                                                     │
│ ❱ 251 │   │   raise ValueError(                                                                  │
│   252 │   │   │   "Relation endpoints need to be formatted as 'app_name:endpoint_name', "
│   253 │   │   │   f"e.g. 'traefik:ingress'. Not: {obj!r}, {other_obj!r}"
│   254 │   │   )                                                                                  │
│                                                                                                  │
│ ╭─────────────────────────── locals ────────────────────────────╮                                │
│ │ include_default_juju_keys = False                             │                                │
│ │                     model = None                              │                                │
│ │                       obj = 'zookeeper:cos-agent'             │                                │
│ │                 other_obj = 'grafana-agent'                   │                                │
│ │             relation_type = <RelationType.regular: 'regular'> │                                │
│ │             this_endpoint = 'cos-agent'                       │                                │
│ │                  this_url = 'zookeeper'                       │                                │
│ ╰───────────────────────────────────────────────────────────────╯                                │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
ValueError: Relation endpoints need to be formatted as 'app_name:endpoint_name', e.g. 'traefik:ingress'. Not: 'zookeeper:cos-agent', 
'grafana-agent'

I'm using 0.3.14

Permission denied for sync scp

Juju only has permission to scp as ubuntu user. Since the charm code on the unit is owned by root, it's not possible for the sync command to scp the files. This is running on LXC.

Example:

$ jhack sync --dry-run sftp-server/0 -s .
watching: 
	/home/joakim/versioned/ops/juju/charms/sftp-server/src/charm.py
Ctrl+C to interrupt
would scp: /home/joakim/versioned/ops/juju/charms/sftp-server/src/charm.py --> sftp-server/0:/var/lib/juju/agents/unit-sftp-server-0/charm/src/charm.py

And doing it with juju scp:

$ juju scp /home/joakim/versioned/ops/juju/charms/sftp-server/src/charm.py sftp-server/0:/var/lib/juju/agents/unit-sftp-server-0/charm/src/charm.py
ERROR exit status 1 (scp: /var/lib/juju/agents/unit-sftp-server-0/charm/src/charm.py: Permission denied)

jenv assumes multipass is installed and crashes if it is not

dylan@protostar:~$ jhack jenv
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /home/dylan/repos/jhack/venv/lib/python3.10/site-packages/jhack/utils/print_env.py:101 in        │
│ print_env                                                                                        │
│                                                                                                  │
│    98 │   │   f"{python_v.major}.{python_v.minor}.{python_v.micro} ({sys.executable})"           │
│    99 │   )                                                                                      │
│   100 │                                                                                          │
│ ❱ 101 │   multipass_version = get_multipass_version()                                            │
│   102 │   data = {                                                                               │
│   103 │   │   "jhack": get_jhack_version(),                                                      │
│   104 │   │   "python": python_version,                                                          │
│                                                                                                  │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │         format = <Format.auto: 'auto'>                                                       │ │
│ │       python_v = sys.version_info(major=3, minor=10, micro=6, releaselevel='final',          │ │
│ │                  serial=0)                                                                   │ │
│ │ python_version = '3.10.6 (/home/dylan/repos/jhack/venv/bin/python3)'                         │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                                  │
│ /home/dylan/repos/jhack/venv/lib/python3.10/site-packages/jhack/utils/print_env.py:79 in         │
│ get_multipass_version                                                                            │
│                                                                                                  │
│    76 def get_multipass_version():                                                               │
│    77 │   """Multipass --version."""                                                             │
│    78 │   try:                                                                                   │
│ ❱  79 │   │   multipass_version = get_output("multipass version --format json")                  │
│    80 │   except subprocess.CalledProcessError:                                                  │
│    81 │   │   logger.info("multipass not found")                                                 │
│    82 │   │   multipass_version = None                                                           │
│                                                                                                  │
│ /home/dylan/repos/jhack/venv/lib/python3.10/site-packages/jhack/utils/print_env.py:24 in         │
│ get_output                                                                                       │
│                                                                                                  │
│    21                                                                                            │
│    22 def get_output(command: str) -> Optional[str]:                                             │
│    23 │   try:                                                                                   │
│ ❱  24 │   │   p = subprocess.run(command.split(), capture_output=True, text=True)                │
│    25 │   │   return p.stdout.strip() if p.returncode == 0 else None                             │
│    26 │   except subprocess.CalledProcessError as e:                                             │
│    27 │   │   logger.info(e)                                                                     │
│                                                                                                  │
│ ╭────────────────── locals ───────────────────╮                                                  │
│ │ command = 'multipass version --format json' │                                                  │
│ ╰─────────────────────────────────────────────╯                                                  │
│                                                                                                  │
│ /usr/lib/python3.10/subprocess.py:501 in run                                                     │
│                                                                                                  │
│    498 │   │   kwargs['stdout'] = PIPE                                                           │
│    499 │   │   kwargs['stderr'] = PIPE                                                           │
│    500 │                                                                                         │
│ ❱  501 │   with Popen(*popenargs, **kwargs) as process:                                          │
│    502 │   │   try:                                                                              │
│    503 │   │   │   stdout, stderr = process.communicate(input, timeout=timeout)                  │
│    504 │   │   except TimeoutExpired as exc:                                                     │
│                                                                                                  │
│ ╭───────────────────────────── locals ─────────────────────────────╮                             │
│ │ capture_output = True                                            │                             │
│ │          check = False                                           │                             │
│ │          input = None                                            │                             │
│ │         kwargs = {'text': True, 'stdout': -1, 'stderr': -1}      │                             │
│ │      popenargs = (['multipass', 'version', '--format', 'json'],) │                             │
│ │        timeout = None                                            │                             │
│ ╰──────────────────────────────────────────────────────────────────╯                             │
│                                                                                                  │
│ /usr/lib/python3.10/subprocess.py:969 in __init__                                                │
│                                                                                                  │
│    966 │   │   │   │   │   self.stderr = io.TextIOWrapper(self.stderr,                           │
│    967 │   │   │   │   │   │   │   encoding=encoding, errors=errors)                             │
│    968 │   │   │                                                                                 │
│ ❱  969 │   │   │   self._execute_child(args, executable, preexec_fn, close_fds,                  │
│    970 │   │   │   │   │   │   │   │   pass_fds, cwd, env,                                       │
│    971 │   │   │   │   │   │   │   │   startupinfo, creationflags, shell,                        │
│    972 │   │   │   │   │   │   │   │   p2cread, p2cwrite,                                        │
│                                                                                                  │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │               args = ['multipass', 'version', '--format', 'json']                            │ │
│ │            bufsize = -1                                                                      │ │
│ │            c2pread = 3                                                                       │ │
│ │           c2pwrite = 4                                                                       │ │
│ │          close_fds = True                                                                    │ │
│ │      creationflags = 0                                                                       │ │
│ │                cwd = None                                                                    │ │
│ │           encoding = 'locale'                                                                │ │
│ │                env = None                                                                    │ │
│ │             errors = None                                                                    │ │
│ │            errread = 5                                                                       │ │
│ │           errwrite = 6                                                                       │ │
│ │         executable = None                                                                    │ │
│ │       extra_groups = None                                                                    │ │
│ │                  f = <_io.TextIOWrapper name=5 encoding='UTF-8'>                             │ │
│ │                gid = None                                                                    │ │
│ │               gids = None                                                                    │ │
│ │              group = None                                                                    │ │
│ │     line_buffering = False                                                                   │ │
│ │            p2cread = -1                                                                      │ │
│ │           p2cwrite = -1                                                                      │ │
│ │           pass_fds = ()                                                                      │ │
│ │           pipesize = -1                                                                      │ │
│ │         preexec_fn = None                                                                    │ │
│ │    restore_signals = True                                                                    │ │
│ │               self = <Popen: returncode: 255 args: ['multipass', 'version', '--format',      │ │
│ │                      'json']>                                                                │ │
│ │              shell = False                                                                   │ │
│ │  start_new_session = False                                                                   │ │
│ │        startupinfo = None                                                                    │ │
│ │             stderr = -1                                                                      │ │
│ │              stdin = None                                                                    │ │
│ │             stdout = -1                                                                      │ │
│ │               text = True                                                                    │ │
│ │                uid = None                                                                    │ │
│ │              umask = -1                                                                      │ │
│ │ universal_newlines = None                                                                    │ │
│ │               user = None                                                                    │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                                  │
│ /usr/lib/python3.10/subprocess.py:1845 in _execute_child                                         │
│                                                                                                  │
│   1842 │   │   │   │   │   │   err_filename = orig_executable                                    │
│   1843 │   │   │   │   │   if errno_num != 0:                                                    │
│   1844 │   │   │   │   │   │   err_msg = os.strerror(errno_num)                                  │
│ ❱ 1845 │   │   │   │   │   raise child_exception_type(errno_num, err_msg, err_filename)          │
│   1846 │   │   │   │   raise child_exception_type(err_msg)                                       │
│   1847                                                                                           │
│   1848                                                                                           │
│                                                                                                  │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │                    args = ['multipass', 'version', '--format', 'json']                       │ │
│ │                 c2pread = 3                                                                  │ │
│ │                c2pwrite = 4                                                                  │ │
│ │    child_exception_type = <class 'OSError'>                                                  │ │
│ │ child_exec_never_called = False                                                              │ │
│ │               close_fds = True                                                               │ │
│ │           creationflags = 0                                                                  │ │
│ │                     cwd = None                                                               │ │
│ │                     env = None                                                               │ │
│ │                env_list = None                                                               │ │
│ │            err_filename = 'multipass'                                                        │ │
│ │                 err_msg = 'No such file or directory'                                        │ │
│ │               errno_num = 2                                                                  │ │
│ │            errpipe_data = bytearray(b'OSError:2:')                                           │ │
│ │            errpipe_read = 7                                                                  │ │
│ │           errpipe_write = 8                                                                  │ │
│ │                 errread = 5                                                                  │ │
│ │                errwrite = 6                                                                  │ │
│ │          exception_name = bytearray(b'OSError')                                              │ │
│ │              executable = b'multipass'                                                       │ │
│ │         executable_list = (                                                                  │ │
│ │                           │   b'/home/dylan/repos/jhack/venv/bin/multipass',                 │ │
│ │                           │   b'/home/dylan/.local/bin/multipass',                           │ │
│ │                           │   b'/home/dylan/bin/multipass',                                  │ │
│ │                           │   b'/home/dylan/bin/multipass',                                  │ │
│ │                           │   b'/usr/local/sbin/multipass',                                  │ │
│ │                           │   b'/usr/local/bin/multipass',                                   │ │
│ │                           │   b'/usr/sbin/multipass',                                        │ │
│ │                           │   b'/usr/bin/multipass',                                         │ │
│ │                           │   b'/sbin/multipass',                                            │ │
│ │                           │   b'/bin/multipass',                                             │ │
│ │                           │   ... +3                                                         │ │
│ │                           )                                                                  │ │
│ │             fds_to_keep = {8}                                                                │ │
│ │                     gid = None                                                               │ │
│ │                    gids = None                                                               │ │
│ │               hex_errno = bytearray(b'2')                                                    │ │
│ │        low_fds_to_close = []                                                                 │ │
│ │         orig_executable = 'multipass'                                                        │ │
│ │                 p2cread = -1                                                                 │ │
│ │                p2cwrite = -1                                                                 │ │
│ │                    part = b''                                                                │ │
│ │                pass_fds = ()                                                                 │ │
│ │                     pid = 1307211                                                            │ │
│ │              preexec_fn = None                                                               │ │
│ │         restore_signals = True                                                               │ │
│ │                    self = <Popen: returncode: 255 args: ['multipass', 'version', '--format', │ │
│ │                           'json']>                                                           │ │
│ │                   shell = False                                                              │ │
│ │       start_new_session = False                                                              │ │
│ │             startupinfo = None                                                               │ │
│ │                     sts = 65280                                                              │ │
│ │                     uid = None                                                               │ │
│ │                   umask = -1                                                                 │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
FileNotFoundError: [Errno 2] No such file or directory: 'multipass'

`hack fire config-server-one/0 <rel-interface>-config-changed` doesn't fire an event

Environment

  • Juju 3.1.6
  • Ubuntu 22.04
  • jhack with latest/edge: 0.3.22 2023-10-06 (264) 95MB -
  • LXD cloud

Deployment

MongoDB Charm Sharded Cluster

juju deploy ./*charm --config role="config-server" config-server-one -n2
juju deploy ./*charm --config role="shard" shard-one -n2
juju deploy ./*charm --config role="shard" shard-two -n2
juju integrate config-server-one:config-server shard-one:sharding
juju integrate config-server-one:config-server shard-two:sharding

Testing

  1. open debug hooks sessions in two separate tabs to monitor events
juju debug-hooks  shard-two/0
juju debug-hooks  shard-two/1
  1. attempt to trigger events:
jhack fire config-server-one/0 sharding-config-changed
jhack fire shard-two/0 sharding-config-changed
jhack fire shard-two/1 sharding-config-changed

Excepted action

sharding-config-changed to be triggered and seen on debug hooks

Observed action

no events triggered

Add optional argument `tail --file FILENAME` to accept logs from one or more files

It would be helpful if tail could optionally parse juju debug-log data from files rather than just directly from juju debug-log. For example, this would be great when debugging something for someone else who sends you a txt file of the logs.

Some example usages:

  • tail --file myFile.txt
    • loads logs from myFile.txt and displays them the same as if the input came directly from juju debug-log
  • tail --file myFile1.txt --file myFile2.txt ...
    • loads logs from multiple files, showing them as if they came from juju debug-log (including ordering them in an interlaced, chronological order and not one file after the other)

show-relation fails with KeyError: 'applications'

Traceback:
jhack show-relation vault:secrets barbican-vault:secrets-storage

╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /snap/jhack/205/lib/python3.8/site-packages/jhack/utils/show_relation.py:651 in                  │
│ sync_show_relation                                                                               │
│                                                                                                  │
│   648 │                                                                                          │
│   649 │   $ jhack utils show-relation my_app:relation_name other_app:other_name                  │
│   650 │   """                                                                                    │
│ ❱ 651 │   return _sync_show_relation(                                                            │
│   652 │   │   endpoint1=endpoint1,                                                               │
│   653 │   │   endpoint2=endpoint2,                                                               │
│   654 │   │   n=n,                                                                               │
│                                                                                                  │
│ ╭──────────────────────── locals ────────────────────────╮                                       │
│ │               color = 'auto'                           │                                       │
│ │           endpoint1 = 'vault:secrets'                  │                                       │
│ │           endpoint2 = 'barbican-vault:secrets-storage' │                                       │
│ │ hide_empty_databags = False                            │                                       │
│ │               model = None                             │                                       │
│ │                   n = None                             │                                       │
│ │      show_juju_keys = False                            │                                       │
│ │               watch = False                            │                                       │
│ ╰────────────────────────────────────────────────────────╯                                       │
│                                                                                                  │
│ /snap/jhack/205/lib/python3.8/site-packages/jhack/utils/show_relation.py:688 in                  │
│ _sync_show_relation                                                                              │
│                                                                                                  │
│   685 │   while True:                                                                            │
│   686 │   │   start = time.time()                                                                │
│   687 │   │                                                                                      │
│ ❱ 688 │   │   table = asyncio.run(                                                               │
│   689 │   │   │   render_relation(                                                               │
│   690 │   │   │   │   endpoint1,                                                                 │
│   691 │   │   │   │   endpoint2,                                                                 │
│                                                                                                  │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │               color = 'auto'                                                                 │ │
│ │             Console = <class 'rich.console.Console'>                                         │ │
│ │             console = <console width=211 ColorSystem.EIGHT_BIT>                              │ │
│ │           endpoint1 = 'vault:secrets'                                                        │ │
│ │           endpoint2 = 'barbican-vault:secrets-storage'                                       │ │
│ │ hide_empty_databags = False                                                                  │ │
│ │               model = None                                                                   │ │
│ │                   n = None                                                                   │ │
│ │                rich = <module 'rich' from                                                    │ │
│ │                       '/snap/jhack/205/lib/python3.8/site-packages/rich/__init__.py'>        │ │
│ │      show_juju_keys = False                                                                  │ │
│ │               start = 1679397896.4327023                                                     │ │
│ │               watch = False                                                                  │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                                  │
│ /usr/lib/python3.8/asyncio/runners.py:44 in run                                                  │
│                                                                                                  │
│   41 │   │   events.set_event_loop(loop)                                                         │
│   42 │   │   if debug is not None:                                                               │
│   43 │   │   │   loop.set_debug(debug)                                                           │
│ ❱ 44 │   │   return loop.run_until_complete(main)                                                │
│   45 │   finally:                                                                                │
│   46 │   │   try:                                                                                │
│   47 │   │   │   _cancel_all_tasks(loop)                                                         │
│                                                                                                  │
│ ╭──────────────────────────────── locals ────────────────────────────────╮                       │
│ │ debug = None                                                           │                       │
│ │  loop = <_UnixSelectorEventLoop running=False closed=True debug=False> │                       │
│ │  main = <coroutine object render_relation at 0x7fb05c2c1040>           │                       │
│ ╰────────────────────────────────────────────────────────────────────────╯                       │
│                                                                                                  │
│ /usr/lib/python3.8/asyncio/base_events.py:616 in run_until_complete                              │
│                                                                                                  │
│    613 │   │   if not future.done():                                                             │
│    614 │   │   │   raise RuntimeError('Event loop stopped before Future completed.')             │
│    615 │   │                                                                                     │
│ ❱  616 │   │   return future.result()                                                            │
│    617 │                                                                                         │
│    618 │   def stop(self):                                                                       │
│    619 │   │   """Stop running the event loop.                                                   │
│                                                                                                  │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │   future = <Task finished name='Task-1' coro=<render_relation() done, defined at             │ │
│ │            /snap/jhack/205/lib/python3.8/site-packages/jhack/utils/show_relation.py:473>     │ │
│ │            exception=KeyError('applications')>                                               │ │
│ │ new_task = True                                                                              │ │
│ │     self = <_UnixSelectorEventLoop running=False closed=True debug=False>                    │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                                  │
│ /snap/jhack/205/lib/python3.8/site-packages/jhack/utils/show_relation.py:533 in render_relation  │
│                                                                                                  │
│   530 │   │   if not (endpoint1 and endpoint2):                                                  │
│   531 │   │   │   raise RuntimeError("invalid usage: provide two endpoints.")                    │
│   532 │   │                                                                                      │
│ ❱ 533 │   │   data = get_relation_data(                                                          │
│   534 │   │   │   provider_endpoint=endpoint1,                                                   │
│   535 │   │   │   requirer_endpoint=endpoint2,                                                   │
│   536 │   │   │   include_default_juju_keys=include_default_juju_keys,                           │
│                                                                                                  │
│ ╭─────────────────────────── locals ───────────────────────────╮                                 │
│ │                 endpoint1 = 'vault:secrets'                  │                                 │
│ │                 endpoint2 = 'barbican-vault:secrets-storage' │                                 │
│ │       hide_empty_databags = False                            │                                 │
│ │ include_default_juju_keys = False                            │                                 │
│ │                     model = None                             │                                 │
│ │                         n = None                             │                                 │
│ │             relation_type = None                             │                                 │
│ ╰──────────────────────────────────────────────────────────────╯                                 │
│                                                                                                  │
│ /snap/jhack/205/lib/python3.8/site-packages/jhack/utils/show_relation.py:398 in                  │
│ get_relation_data                                                                                │
│                                                                                                  │
│   395 │                                                                                          │
│   396 │   >>> get_relation_data('prometheus/0:ingress', 'traefik/1:ingress-per-unit')            │
│   397 │   """                                                                                    │
│ ❱ 398 │   provider_data = get_content(                                                           │
│   399 │   │   provider_endpoint, requirer_endpoint, include_default_juju_keys, model=model       │
│   400 │   )                                                                                      │
│   401 │   requirer_data = get_content(                                                           │
│                                                                                                  │
│ ╭─────────────────────────── locals ───────────────────────────╮                                 │
│ │ include_default_juju_keys = False                            │                                 │
│ │                     model = None                             │                                 │
│ │         provider_endpoint = 'vault:secrets'                  │                                 │
│ │         requirer_endpoint = 'barbican-vault:secrets-storage' │                                 │
│ ╰──────────────────────────────────────────────────────────────╯                                 │
│                                                                                                  │
│ /snap/jhack/205/lib/python3.8/site-packages/jhack/utils/show_relation.py:258 in get_content      │
│                                                                                                  │
│   255 │   other_app_name, _ = other_url.split("/") if "/" in other_url else (other_url, None)    │
│   256 │   this_app_name, _ = this_url.split("/") if "/" in this_url else (this_url, None)        │
│   257 │                                                                                          │
│ ❱ 258 │   app_name, units, meta = get_app_name_and_units(                                        │
│   259 │   │   this_url, this_endpoint, other_app_name, other_endpoint, model                     │
│   260 │   )                                                                                      │
│   261                                                                                            │
│                                                                                                  │
│ ╭─────────────────────────── locals ────────────────────────────╮                                │
│ │                         _ = None                              │                                │
│ │ include_default_juju_keys = False                             │                                │
│ │                     model = None                              │                                │
│ │                       obj = 'vault:secrets'                   │                                │
│ │            other_app_name = 'barbican-vault'                  │                                │
│ │            other_endpoint = 'secrets-storage'                 │                                │
│ │                 other_obj = 'barbican-vault:secrets-storage'  │                                │
│ │                 other_url = 'barbican-vault'                  │                                │
│ │             relation_type = <RelationType.regular: 'regular'> │                                │
│ │             this_app_name = 'vault'                           │                                │
│ │             this_endpoint = 'secrets'                         │                                │
│ │                  this_url = 'vault'                           │                                │
│ ╰───────────────────────────────────────────────────────────────╯                                │
│                                                                                                  │
│ /snap/jhack/205/lib/python3.8/site-packages/jhack/utils/show_relation.py:228 in                  │
│ get_app_name_and_units                                                                           │
│                                                                                                  │
│   225 │   """Get app name and unit count from url; url is either `app_name/0` or `app_name`.""   │
│   226 │   app_name, unit_id = url.split("/") if "/" in url else (url, None)                      │
│   227 │                                                                                          │
│ ❱ 228 │   meta = get_metadata_from_status(                                                       │
│   229 │   │   app_name, relation_name, other_app_name, other_relation_name, model=model          │
│   230 │   )                                                                                      │
│   231 │   if unit_id is not None:                                                                │
│                                                                                                  │
│ ╭──────────────── locals ─────────────────╮                                                      │
│ │            app_name = 'vault'           │                                                      │
│ │               model = None              │                                                      │
│ │      other_app_name = 'barbican-vault'  │                                                      │
│ │ other_relation_name = 'secrets-storage' │                                                      │
│ │       relation_name = 'secrets'         │                                                      │
│ │             unit_id = None              │                                                      │
│ │                 url = 'vault'           │                                                      │
│ ╰─────────────────────────────────────────╯                                                      │
│                                                                                                  │
│ /snap/jhack/205/lib/python3.8/site-packages/jhack/utils/show_relation.py:176 in                  │
│ get_metadata_from_status                                                                         │
│                                                                                                  │
│   173 ):                                                                                         │
│   174 │   status = _juju_status(app_name, model=model, json=True)                                │
│   175 │   # machine status json output apparently has no 'scale'... -_-                          │
│ ❱ 176 │   app_status = status["applications"][app_name]                                          │
│   177 │   if app_status.get('subordinate-to'):                                                   │
│   178 │   │   units = {}                                                                         │
│   179 │   │   for other_unit in status["applications"][other_app_name]['units'].values():        │
│                                                                                                  │
│ ╭──────────────── locals ─────────────────╮                                                      │
│ │            app_name = 'vault'           │                                                      │
│ │               model = None              │                                                      │
│ │      other_app_name = 'barbican-vault'  │                                                      │
│ │ other_relation_name = 'secrets-storage' │                                                      │
│ │       relation_name = 'secrets'         │                                                      │
│ │              status = {}                │                                                      │
│ ╰─────────────────────────────────────────╯                                                      │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯

[Feature Request] Allow hook filtering in `jhack tail`

When monitoring models with a fast update_status hook (such as test runs that use ops_test.fast_forward(), the tail command gets spammed with update_status hooks and it becomes unreadable. Could a flag be included in this command to include or exclude certain hooks?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.