This is a quick guide on how to connect a remote debugger to a charm, I will be using VS Code. A similar approach could be used to get something working on PyCharm (sorta, see section at the bottom). I had to adapt this a little since I run Juju in a VM, so this is a slightly simplified version of what I’ve been doing. Hopefully I didn’t miss anything when I “simplified” it, but if I did please let me know.
This shouldn’t take more than a few minutes to get running (minus the time to pack the charm). It’s pretty straight-forward.
Add the hooks and re-pack the charm
-
Add
debugpy
to yourrequirements.txt
-
Create a file in your charm’s
src/
directory, for examplesrc/remote_debug.py
, and copy the following contents to it. This will keep most of the changes in a separate file and make it easier to include as you need it. I add this file to my local ignore (.git/info/exclude
) and then forget it exists until I need it.import logging from typing import Optional import debugpy # type: ignore import ops logger = logging.getLogger(__name__) class RemoteDebuggerCharmBase(ops.CharmBase): def __init__(self, *args, **kwargs): _patch_breakpoint_hook() super().__init__(*args, **kwargs) def _wait_for_debugger(name: Optional[str] = None): if not debugpy.is_client_connected(): debugpy.listen(("0.0.0.0", 5678)) logger.info("Waiting for debugger to attach") debugpy.wait_for_client() def _runcall_patch(*args, **kwds): _wait_for_debugger() # TODO: It would be better to call set_trace() here, but I couldn't get it # to work with the debugger return args[0](*args[1:], **kwds) def _patch_breakpoint_hook(): logger.info("Installing breakpoint() hook") # Installs a hook, so that we block when we hit a breakpoint that we told # juju to watch using `juju debug-code --at=hook <unit> <event-hook>` ops.framework.pdb.runcall = _runcall_patch
-
Import
RemoteDebuggerCharmBase
and inherit from it in the charm you would like to debug. For example, if I’m debugging the vault-k8s charm, I would replace this line in charm.py:class VaultCharm(CharmBase):
With
class VaultCharm(RemoteDebuggerCharmBase):
-
Repack the charm
charmcraft pack
Deploy the modified charm
juju deploy ./vault-k8s_ubuntu-22.04-amd64.charm vault-k8s --trust -n 5 --resource=vault-image=ghcr.io/canonical/vault:1.15.3
Add the debugging config to vscode
Get the IP address of the unit you would like to debug
unit_name="vault-k8s/1"
juju show-unit "${unit_name}" | yq ".*.address"
Add the the following to your launch.json
file. Make sure to replace the host
key with the IP address you got above.
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Remote Attach",
"type": "python",
"request": "attach",
"connect": {
"host": "<UNIT IP ADDRESS>",
"port": 5678
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "."
}
],
"justMyCode": true
}
]
}
Tell juju to debug the hook, and wait for it to fire
juju debug-code --at=hook "${unit_name}" update-status
This will cause juju to launch a tmux session and wait for the hook to be run. You may be familiar with the debug-code command already. But with the RemoteDebuggerCharmBase
, instead of launching pdb when the hook is run, you should instead see a message that says “Waiting for debugger to attach”.
Since we’re debugging update-status
, we can make this happen a little faster by speeding up the update interval
juju model-config update-status-hook-interval=5s
Connect VS Code
At this point, you can click the “play” button in VS Code, and it should connect to the remote debugger. However, once it connects it will immediately resume running. So, first set a breakpoint in VS Code. Otherwise, it should stop if an exception is thrown.
If everything worked out, you can now step through the code. You may need to add some pathMappings
to the launch.json
file to jump into library code correctly but anything in src/
should be working.
Consider using something like jhack to sync your code when you make changes.
Re: PyCharm
As I understand it, PyCharm doesn’t support the “Debug Adapter Protocol” that debugpy
implements, so you would likely need to use pydevd
. pydevd
in turn doesn’t support connections from IDE → debugger, so you’d need to reverse the direction of the connection and have the debugger contact the IDE.