This is a homegrown collection of opinionated scripts and utilities to make the charm dev's life somewhat easier.
Clone the repo; alias '/path/to/jhack/main.py' as 'jhack', or something. Ensure the requirements are installed:
$ pip install -r requirements.txt
$ python setup.py bdist_wheel
$ pip install ./dist/jhack-v...whl
$ sudo snap install --edge jhack
$ sudo snap connect jhack:dot-local-share-juju snapd
jhack [category] [command]
for example:
$ jhack utils tail
$ jhack model rm
Happy hacking!
jhack utils sync ./src application-name/0
Will watch the ./src folder for changes and push any to application-name/0 under /charm/src/.
jhack utils unbork-juju
Does exactly what it says, and it does it pretty well.
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.
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 │
└───────────┴──────────────────────────────┴──────────────────────────────┘
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?
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 │
└────────────────┴──────────────────────────────────────┴─────────────────────────┘
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: │ │ │
│ │ │ https://my.it:80/test-charm-ipu-9dg8… │ │ │
│ │ │ ipu/1: │ │ │
│ │ │ url: │ │ │
│ │ │ https://my.it:80/test-charm-ipu-9dg8… │ │ │
│ │ │ ipu/2: │ │ │
│ │ │ url: │ │ │
│ │ │ https://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.
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', │ │ │ │ 'https://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'}}} │ │ │
│ └───────────────────────┴─────────────────────────────────┘ │ │
└─────────────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────┘
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: