How to set a charm's status

Contents:

Set unit status

To set unit status, the charm should assign an instance of a StatusBase subclass to the Unit.status property such as ActiveStatus. For example:

def _on_install(self, event: ops.InstallEvent):
    self.unit.status = ops.MaintenanceStatus("Configuring Prometheus")
    # ... perform install actions ...
    self.unit.status = ops.ActiveStatus()

If you’re curious to know how this works under the hood:
When you’re assigning Unit.status, this uses Juju’s status-set hook tool.

Set application status

Setting application status is similar: assign it to the Application.status property (this uses status-set --application under the hood). Only the leader unit can set application status. For example:

def _on_config_changed(self, event: ops.ConfigChangedEvent):
    if not self.unit.is_leader():
        return
    if "port" not in self.model.config:
        self.app.status = ops.BlockedStatus('"port" required')
        return
    # ... update port ...
    self.app.status = ops.ActiveStatus()

If a charm sets application status explicitly, that status will appear as the application status in juju status output. However, if a charm does not set application status, the application status displayed is the highest-priority status of all the units (priorities are listed above).

Charms for applications that are intended to scale up to more than one unit should usually set application status explicitly. For example, a horizontally-scaled web app charm could remain up even if 1 of 3 units is not responding; the overall application is still “active”.

Evaluate charm status across multiple components

As of version 2.5.0, ops includes two events that allow a charm to automatically set status at the end of every hook. These are collect_app_status and collect_unit_status, both of which are of CollectStatusEvent type.

These events are triggered by the ops framework at the end of every successful hook to collect statuses for evaluation (collect_app_status will only be triggered on the leader unit). If any statuses were added by the event handlers using add_status, the framework will choose the highest-priority status and set that as the status.

The status-collecting events allow a charm to provide a status for each component of the charm, and let the framework figure out which status to set (and show in juju status output). The system is flexible – a charm can have multiple collect_unit_status (or collect_app_status handlers), and each handler can add one or more statuses for evaluation.

Here is an example of a charm with two components, a “database” and a “web app” component. The database component adds collect_unit_status handlers for two sub-components: relation status, and config status. The web app component has a single handler for config status:

class StatustestCharm(ops.CharmBase):
    def __init__(self, *args):
        super().__init__(*args)
        self.database = Database(self)
        self.webapp = Webapp(self)


class Database(ops.Object):
    """Database component."""

    def __init__(self, charm: ops.CharmBase):
        super().__init__(charm, "database")
        self.framework.observe(charm.on.config_changed, self._on_config_changed)

        # Note that you can have multiple collect_status observers even
        # within a single component, as shown here. Alternatively, we could
        # do both of these tests within a single handler.
        self.framework.observe(charm.on.collect_unit_status, self._on_collect_db_status)
        self.framework.observe(charm.on.collect_unit_status,
                               self._on_collect_config_status)

    def _on_collect_db_status(self, event: ops.CollectStatusEvent):
        if 'db' not in self.model.relations:
            event.add_status(ops.BlockedStatus('please integrate with database'))
            return
        event.add_status(ops.ActiveStatus())

    def _on_collect_config_status(self, event: ops.CollectStatusEvent):
        message = self._validate_config()
        if message is not None:
            event.add_status(ops.BlockedStatus(message))
            return
        event.add_status(ops.ActiveStatus())

    def _on_config_changed(self, event):
        if self._validate_config() is not None:
            return
        mode = self.model.config["database_mode"]
        logger.info("Database using mode %r", mode)

    def _validate_config(self) -> typing.Optional[str]:
        """Validate charm config for the database component.

        Return an error message if the config is incorrect, None if it's valid.
        """
        if "database_mode" not in self.model.config:
            return '"database_mode" required'
        return None


class Webapp(ops.Object):
    """Web app component."""

    def __init__(self, charm: ops.CharmBase):
        super().__init__(charm, "webapp")
        self.framework.observe(charm.on.config_changed, self._on_config_changed)
        self.framework.observe(charm.on.collect_unit_status, self._on_collect_status)

    def _on_collect_status(self, event: ops.CollectStatusEvent):
        message = self._validate_config()
        if message is not None:
            event.add_status(ops.BlockedStatus(message))
            return
        event.add_status(ops.ActiveStatus())

    def _on_config_changed(self, event):
        if self._validate_config() is not None:
            return
        mode = self.model.config["port"]
        logger.info("Web app using port %d", mode)

    def _validate_config(self) -> typing.Optional[str]:
        """Validate charm config for the web app component.

        Return an error message if the config is incorrect, None if it's valid.
        """
        if "port" not in self.model.config:
            return '"port" required'
        return None