Introducing `jhack script`: AKA on-the-fly debugging actions for charms

This command used to be called jhack crpc but has been renamed to the more apt script. the jhack crpc name has been assigned to a different command.

Say you’re developing a charm, or you’re facing an issue with a deployment that you want to inspect a bit better.

It’s often useful to know what data a charm has access to. What relations can it see? What’s the result of calling that one method? What’s the self.foo.library_is_ready() method returning?

Also, it can be useful to manipulate some of that data by hand.

The traditional ways of accessing this information, in order of hackiness, include:

  1. adding a debug log to the charm file, repacking the charm, redeploy/refresh, read the juju debug-log to see what’s up (or jhack sync+jhack fire as a faster alternative)
  2. using juju debug-code to pdb into the process and inspect the variables live
  3. Using juju exec to issue direct hook tool calls such as juju exec --unit foo/0 relation-set ...

Enter jhack script

jhack script is my take on how to quickly execute something charm-side, without having to submit to Juju event’s model (aka without having to wait) and without having to resort to low-level manual hook tool calls.

The base idea is: you write a script, for example:

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'

then you do jhack script myapp/0 --input ./path/to/script.py and you see the output of that script.

Easy!

How about executing a method on a charm? Also easy.

def main(charm):
    relations = charm.model.relations['bar']
    for relation in relations:
        charm._do_something_with(relation, arg=True, kwarg=False)

What’s really going on

Normally, a charm executes when it receives an event. Or when you juju exec ./dispatch. In this case, jhack is generating an alternative dispatch script, one that sets up the framework and instantiates the charm, but does not fire any event on it. Instead of passing it to ops.main.main, it passes the charm instance to the function you defined!

jhack scp’s this script and the modified dispatch to the unit, juju execs the modified dispatch script and the rest is history (also known as stdout).

Use cases

Inspiration for this command was me wanting to prototype a generic debugging action for charms, that is, a script that would collect data using the same API that a charm uses to work with it in the first place. Why use juju show-unit when you already know ops’ API and can do relation.data[relation.app] instead?

Secondly, I also see a role for a battery of custom scripts that we can co-host with our charm repositories to use when things go south. For example a ‘get this charm unstuck’ script for when you’re developing a feature…

Finally, think about how you can use this when you’re developing for example a charm library, or a routine that needs to take the charm instance as input.

class MyCharmLib(ops.Object):
    ... 

def main(charm):
    lib = MyCharmLib(charm)
    if lib.some_api_call(...):
        return 42
    return 41

Or whatever.

Either way, it’s pretty nice.

Oh, and did I mention that if you do jhack script appname --input foo.py it will run the script on all units of that application?

Impress your friends with pipes

As a side note, jhack script can read scripts from stdin, for the quick and dirty party tricks / demo hacks.

image

Thoughts on future work

It would be interesting to add functionality to make scripts ‘sticky’, i.e. upload them to the unit and have them run as a pre/post event hook on every subsequent dispatch call. This would however mean adding some serious complexity and potential for errors.

Also see: Introducing `jhack crpc`: aka python evaluation on live charms

6 Likes

Hi @ppasotti, this is great! Somewhat surprising that no one has written a tool like this before. Is this coming to a snap near me soon? (I can run it from source, but I tried the edge snap and it’s not there yet.)

Small nits on the UX:

  • Given that one will almost always specify the script, would jhack script app/0 [script.py] be a slightly nicer UI? You could still leave it off and get stdin, but no need for --input or -i.
  • I think crpc might be even more useful – I’ve often wanted to do quick checks like that. I guess the first “c” in “crpc” means “charm”, but it seems to me that’s kinda redundant in this context. What about just jhack rpc, or even jhack eval? (It’s very similar to Python’s own eval but for a charm.)
2 Likes

good feedback! Done and done :slight_smile: it should have been on edge already, releasing it now.