About
This wiki page is a collection of tips for starting out with the Operator Framework. The Operator Framework is a new approach for writing charms that’s easy for development and maintenance.
If you have an improvement to make, please click the “Edit” button and make it. If you have a question that you would like answered, please add it below.
First steps with the Operator Framework
Getting set up
The entrypoint is the charmcraft
tool. The recommended way to install it is via snap
:
$ sudo snap install charmcraft --beta
Alternatively, you can use pip
provided by Python3:
$ pip3 install --user charmcraft
Creating a charm
The minimal charm contains a metadata.yaml
file (reference), a requirements.txt
file containing the ops
framework, and an executable src/charm.py
file as the entry point for the charm. However, a better starting point is to use charmcraft init
to initialize a new charm based on the current directory name with the recommended additional files (see charmcraft init --help
for additional options).
Questions
Can a charm target Kubernetes as well as other clouds at the same time?
No, at least not at this stage.
How do I write log messages?
Use the standard Python idioms.
#! /usr/bin/env python3
# src/charm.py
from ops.charm import CharmBase
from ops.model import ActiveStatus
from ops.main import main
import logging
class LoadReporter(CharmBase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.log = logging.getLogger(self.__class__.__name__)
self.framework.observe(self.on.update_status, self.on_update_status)
def on_update_status(self, _event):
with open('/proc/loadavg') as f:
load = f.read().strip()
self.log.info(f"/proc/loadavg: {load}")
if __name__ == "__main__":
main(LoadReporter) # main() sets up Juju logging infrastructure
Under the hood, the framework sets up a bespoke handler to communicate with Juju during the call to ops.main.main()
.
How do I access config values?
The CharmBase.config
object is a dictionary representing
the current configuration for the charm. It’s accessed from within
charm code as self.config[key]
. The values returned by the config objects are strings. Manual validation is required (see the next section).
Here is a working demonstration charm (echo
) that shows how to access config parameters:
# config.yaml
options:
message:
type: string
description: Message to be reported in the unit's status
default: ''
# src/charm.py
from ops.charm import CharmBase
from ops.model import ActiveStatus
from ops.main import main
class EchoConfig(CharmBase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.framework.observe(self.on.config_changed, self.on_config_change)
def on_config_change(self, _event):
message = self.model.config['message']
self.unit.status = ActiveStatus(message)
if __name__ == "__main__":
main(EchoConfig)
If you would like to play around with this yourself, take a look at the echo
charm.
How do I determine if the unit is the application “leader”?
Multi-unit applications need to be aware of leadership. The Juju controller nominates one unit as the application’s leader. The leader is able to modify application-level data. If the leader becomes unavailable, Juju will replace it.
The charm’s unit
object provides an is_leader()
method to allow charms to ascertain whether they are running within the leader’s context:
self.unit.is_leader()
For example:
class MultiUnit(CharmBase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.framework.observe(self.on.log_forwarding_relation_created, self.publish_hostname)
def publish_hostname(self, event):
if not self.model.unit.is_leader():
return
hostname = self.config['log_forwarding_hostname']
event.relation.data[self.app]['hostname'] = hostname
Leaders have access to:
- modifying relation data at the application level, via the
relation-set --app
hook tool - setting “leader data”, via the
leader-set
hook tool
Notes:
- While rare, leadership can change during a hook’s execution if the charm runs some particularly long blocking code. It may be useful to use leader data to record what initialization steps the leader has taken or begun if there is a chance they will take a long time.
How do I write an action?
The actions.yaml
file is populated with the action details, e.g.
add-repo:
description: Add a code repository
params:
repo:
type: string
description: Name of the repository.
user:
type: string
description: Name of the user that has access to the repository.
required: [repo, user]
Similar to hook, actions are registered as events, but with a suffix of “_action”, and can be observed just like any other event. The event
object for an action will have a few additional attributes and methods for working with the action:
- params[] A mapping of the parameters the action was called with.
- log() Emit a real-time message to the caller of the action.
- set_result() Set the final result data for the action.
- fail() Report that the action failed, with a message.
For example:
class RepoManager(CharmBase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.framework.observe(self.on.add_repo_action, self.on_add_repo_action)
def on_add_repo_action(self, event):
event.log(f"Got {event.params['repo']} and {event.params['user']}")
if user == "admin":
event.fail(f"Admin user repo is reserved")
else:
event.set_result({"repo": f"/repos/{event.params['user']}"})
How do I open and close ports?
As of 2020-05-10T00:00:00Z, you should call open-port
and close-port
directly from your charm. It’s possible to use small helper functions to assist with this, or import the functionality from charm-helpers
.
from subprocess import run
def _modify_port(start=None, end=None, protocol='tcp', hook_tool="open-port"):
assert protocol in {'tcp', 'udp', 'icmp'}
if protocol == 'icmp':
start = None
end = None
if start and end:
port = f"{start}-{end}/"
elif start:
port = f"{start}/"
else:
port = ""
run([hook_tool, f"{port}{protocol}"])
def enable_ping():
_modify_port(None, None, protocol='icmp', hook_tool="open-port")
def disable_ping():
_modify_port(None, None, protocol='icmp', hook_tool="close-port")
def open_port(start, end=None, protocol="tcp"):
_modify_port(start, end, protocol=protocol, hook_tool="open-port")
def close_port(start, end=None, protocol="tcp"):
_modify_port(start, end, protocol=protocol, hook_tool="close-port")
How do I test charms?
The Operator Framework includes a testing module (ops.testing
) that enables simple unit testing.
The Test Harness is meant to be used as a mocking library that mocks out the Operator Framework’s dependencies. This allows you to easily write tests that are isolated from anything beyond the framework.
Here is an example of how to use it:
# src/charm.py
import sys
sys.path.insert(0, 'lib')
from ops.charm import CharmBase
class MagicCharm(CharmBase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.magic = 0
self.framework.observe(self.on.update_status, self.on_update_status)
def on_update_status(self, _event):
self.magic += 1
# test_charm.py
import unittest
from ops.testing import Harness
from charm import MagicCharm
class CharmTest(unittest.TestCase):
def setUp(self):
self.harness = Harness(MagicCharm)
def test__on_config_changed__does_the_thing_that_we_expect(self):
self.harness.begin()
# Exercise
# Simulate a configuration update to your charm
self.harness.update_config()
# Check if this produced the desired effect
self.assertEqual(harness.charm.magic, 1)
Working documentation on the harness may be found here.
Write a “pod spec” for Kubernetes-based charms
A “pod spec” is a definition of a Kubernetes Pod and its associated resources. To generate one, use the pod_spec_set()
method
# src/charm.py
import sys
sys.path.insert(0, 'lib')
from ops.charm import CharmBase
from ops.model import ActiveStatus
from ops.main import main
class K8sCharm(CharmBase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pod.pod_spec_set(...)
To understand the pod_spec_set()
method, you should review the information provided by in some related posts in Discourse. The method is a thin wrapper around the underlying hook tool set-pod-spec
provided by Juju:
How to install and use Python dependencies, e.g. charmhelpers
and cryptography
As of 2020-05-10T00:00:00Z, you should save a copy of your your dependencies within the ‘lib’ directory, then import them from src/charm.py
. This pattern is known as “vendoring dependencies”.
Troubleshooting
This section is a note of a few things that are worth checking if things aren’t working as expected.
- Check that the top of the
charm.py
contains#!/usr/bin/env python3
. Juju does not have exclusive control over the system’s Python installation. Modifications to the machine’s Python installation can adversely affect charms. - Ensure that the
hooks
directory has symlinks tocharm.py
namedinstall
,upgrade-charm
andstart
. - Check that
charm.py
is marked executable.
TODO
Have you played around with the Operator Framework at all? Can you answer any of these?
- Getting Relation Data
- Setting relation data
Using resources to enable offline deployments(see Pattern: using resources to support offline deployments )- Getting subordinate/juju-info relation data
Changing application-level , aka “leader” dataRendering a pod spec (for k8s charms)- Getting network information
- IP address for daemons to listen on
- IP address to publish to clients
Opening/Closing portsAdding unit testsHow to install and use Python dependencies, e.g. charmhelpers and cryptography- How to make use of StoredState to capture the status of the unit.
Determining if the unit is the application “leader”