This section will provide an overview of Charmed Operator Framework constructs provided to charm authors. Several of these constructs are present in the initial template created by
charmcraft init, so some may look familiar from the “Hello, World” guide.
Full, auto-generated API documentation is available should you require more detail than is provided here.
Main / Entrypoint
The Charmed Operator Framework provides a top-level
main method as part of
ops.main. This method should be the main entrypoint for your charm. The method takes a charm class as an argument, and is used to initialise the charm code when the charm is invoked, and ensure that events are dispatched to the charm in response to controller-emitted events.
This method is imported and called automatically in the template charm like so:
from ops.main import main # ... charm code if __name__ == "__main__": main(HelloCharm)
Charm-specific constructs are imported from
ops.charm. Critically, this part of the library contains the definition of
CharmBase, the base class from which all Charms are formed. All charms written using the Charmed Operator Framework must use this abstraction. Charm authors must ensure that their Charm
__init__ methods invoke
super().__init__ so that the relevant framework events are defined for the new charm.
__init__ method should be used to ensure that the charm observes all events relevant to its operation, and configures default values for any Stored State (see Framework). An example implementation of this from the
charmcraft template is below:
from ops.charm import Charmbase class HelloCharm(CharmBase): def __init__(self, *args): super().__init__(*args) # ...
Each time an event is fired by the Juju controller, the charm’s
__init__ method is called, meaning that a new charm object is created each time it responds to an event. The charm is not a persistent process, which motivates the need for
A reference to the Charmed Operator Framework is bound into each charm class instantiation as
self.framework by the
main method. This abstraction is an object-oriented representation of the framework itself, providing mechanisms for observing events, manipulating charm state, and accessing charm metadata and configuration.
Charm authors should use the framework construct to observe framework events and bind event handlers to them:
from ops.charm import CharmBase, ConfigChangedEvent class HelloCharm(CharmBase): def __init__(self, *args): super().__init__(*args) self.framework.observe(self.on.config_changed, self._on_config_changed) self.framework.observe(self.on.fortune_action, self._on_fortune_action) def _on_config_changed(self, event: ConfigChangedEvent): current = self.model.config["thing"] # ...
In this simple initialisation we create a new charm that observes two events:
fortune-action . These are bound to private methods within the
HelloCharm class, as indicated by the
_ preceding the handler names. By convention, event handlers are usually private methods.
In this example, the
event argument to
_on_config_changed is annotated with the
ConfigChangedEvent type. All events in the Charmed Operator Framework inherit from the
EventBase class. By default it provides three methods, though
defer() is the only one of those designed to be called by a charm author directly. The
defer() method allows developers to put the relevant event handler/callback into a queue such that the same callback will be called again when the charm code is next invoked - where an invocation could be the result of any action or event.
defer() method does not halt execution of the callback, and should nearly always be followed by an explicit
return. When the charm is re-invoked, the callback will execute from the start, not from the point of deferral.
Developers will generally call
defer() as a result of some precondition not being met - though consideration should be given to whether the particular callback should be deferred, or whether it would be better to simply wait for the event to be triggered again.
Accessing charm metadata
self.framework construct also provides a convenient way for charm authors to access a charm’s metadata. This can be accessed either as
self.meta and is an instantiation of the
CharmMeta class that provides properties for all supported metadata fields, as per the specification.
Because charms do not run as a persistent process, the Juju controller and Charmed Operator Framework facilitate access to a per-unit state store, which can be useful for tracking configuration options and other information relevant to the current deployment of the application.
By convention, the state store should be a class attribute on the Charm class named
_stored. This attribute should always be private, hence the preceding
_. Charm state should not be used to track the global state of the application - such as whether an application has started, or whether a particular event has been triggered. Such behaviour will lead to brittle and unpredictable charms.
The Charmed Operator Framework exposes this storage through the
StoredState class which can be initialised like so:
from ops.framework import StoredState # ... class HelloCharm(CharmBase): # Set up the state store _stored = StoredState() # ...
Charm developers are recommended to provide default values for charm state attributes. This initialisation should take place in the charm’s
def __init__(self, *args): super().__init__(*args) # Initialise the 'things' attribute of the state to an empty list self._stored.set_default(things=)
set_default method will check the state returned from previous charm invocations, and will only create a new entry with the default value if the entry does not already exist. The
set_default method does not perform any type checking. Charm authors should take care to consistently assign data of the same type to specific state attributes, or check the type of any returned values where there could be ambiguity.
Once initialised, the store can be accessed much like any other variable in Python:
# Set a value self._stored.things = ["some", "interesting", "items"] # Get a value important_things = self._stored.things
The Charmed Operator Framework will only persist changes in stored state to the Juju storage backend when the lifecycle event from which it is being manipulated returns successfully. If an exception is thrown that causes the event invocation to exit early, the state will not be saved.
The Charmed Operator Framework enables developers to access information about the Juju model into which a charm is deployed. The model can be accessed from within a charm class with
self.model (a property accessor for
Applications and Units
In the context of a Juju model, a charm represents a deployed application. In the case of a database, one might deploy the database application with
juju deploy some-database. That application will comprise of one or more units, which represent individual running instances.
The Charmed Operator Framework gives Charm authors access to representations of the current application and unit through
self.unit (which are property accessors for
self.model.unit respectively). These getters return the
Unit running the code. Charm authors can also access arbitrary applications and units from the model with the
Runtime application configuration is stored in the model; charm authors can access the configuration using the Framework’s model abstraction. An example of this is shown below:
# ... def _on_config_changed(self, event): current_thing = self.model.config["thing"] # ...
There is more information on working with configuration in the Handling configuration section
Each unit of a deployed application is able to report its own status to the Juju controller. There are a total of six valid status types supported, though only four are accessible from charm code. Each status can be set with an accompanying message (shown in example below):
||ActiveStatus is the “green status” for a charm. It signifies that the unit has successfully installed and configured the supported application, and the application has been started.|
||Signifies that the current unit is healthy, but the charm is waiting on a process (such as a package installation). A waiting unit could also be waiting on another (already related) application, such as a database, before it is able to be configured or started by the charm. No action is expected from the administrator when this status is reported.|
||Signifies that the charm has received an event which requires it to temporarily pause or stop the supported application, for example a database migration or package upgrade. A charm indicating
||This is an unhealthy status, and signifies that the charm requires intervention by an administrator before it can start/restore service of the application. This status is commonly set when a charm detects a malformed or missing configuration item; or the charm requires a relation with another application which has not yet been defined by the administrator.|
||Unknown state that is never set from Charm code. New units are deployed initially with
||Error state that is never set from Charm code. This state is set automatically any time a lifecycle event fails to complete successfully. This is usually caused by underlying, uncaught exceptions resulting from code failures (such as incorrect syntax, insufficient bound checking etc.) or network outages.|
Before a status message can be set, the relevant status types must be imported from
from ops.model import ActiveStatus, MaintenanceStatus # ... class HelloCharm(CharmBase): # ... def _on_install(self, event): self.unit.status = MaintenanceStatus("Installing application packages") # ... self.unit.status = ActiveStatus() # ...
ActiveStatus for an application should, in most cases, be set without a message. If there is a pressing need to relay information to the administrator, consider using other logging constructs, or whether indeed the application should be relaying an
ActiveStatus, or reverting to
BlockedStatus to convey information.
model abstraction also understands the workloads that are running, and where Pebble is used the Charmed Operator Framework provides methods to interact with workloads by interacting with the Pebble API. At present, only workloads of type
Container are supported.
Container objects are generally accessed through a property of an event (e.g.
PebbleReadyEvent) or by querying for a container by name with the
Unit.get_container() method. The container name is specified in the
metadata.yaml file in the
containers map (more information later in Running a workload).
Here we consider an example where the workload is running in a container named
# ... def __init__(self, *args): super().__init__(*args) # See 'Running a workload' section for more information on Pebble events self.framework.observe(self.on.pause_pebble_ready, self._on_pause_pebble_ready) self.framework.observe(self.on.config_changed, self._on_config_changed) # ... def _on_pause_pebble_ready(self, event: PebbleReadyEvent) -> None: # Get a reference to the container from the PebbleReadyEvent container = event.workload # ... def _on_config_changed(self, event: ConfigChangedEvent) -> None: # Get a reference to the container from the unit container = self.unit.get_container("<container_name>") # ...
Unit class also provides a
containers property that returns a dictionary of
Containers, ordered by name. The
Container class provides a number of helper methods for interacting with Pebble: modifying Pebble configuration, starting and stopping services, and reading or writing files on the workload container. See the Interacting with Pebble page for details and examples.