Managing Charms with the Charm Framework

Traditional charm authoring is focused on implementing hooks. That is, the charm author is thinking in terms of “What hook am I handling; what does this hook need to do?” However, in most cases, the real question should be “Do I have the information I need to configure and start this piece of software and, if so, what are the steps for doing so?” The charm framework tries to bring the focus to the data and the setup tasks, in the most declarative way possible.

Stop Thinking about Hooks

Consider a charm that has a config option specifying the version of software to install, and which requires both a database and a messaging queue server (such as RabbitMQ) for the software to function. In which hook do you install the software?

If your config option has a default value, you could possibly use the install hook, but then you’d still be duplicating logic in config-changed. And even if you delay install until config-changed, you can’t start the software in start, since you have to wait for the database and messaging servers. And you can’t reliably determine which of those hooks to put your logic in, either, since they could be connected in any order.

Thus, a good design pattern is to have all the logic in a common location, which checks all of the requirements and performs the appropriate actions if they are satisfied. And every hook ends up reduced to:

#!/bin/env python
import common
common.manage()

The Charm Framework then gives you a declarative way to describe what your requirements and actions to take are, as well as what data you provide to other services. It also provides helpers around relations and config to make implementing that logic as easy and self-documenting as possible.

Overview

Charms using the Framework declare one or more components, providing the information mentioned above: what is provided by the component, what is required by the component, and what callbacks should be run to setup (start) and cleanup (stop) the service. (The definition format is fully documented in the Manager class reference.)

An example implementation might be:

from charmhelpers.core import hookenv
from charmhelpers.core.charmframework import Manager
from charmhelpers.core.charmframework import helpers

class HttpRelation(helpers.Relation):
    relation_name = 'website'

    def provide_data(self, remote_service, ready):
        return {
            'hostname': hookenv.unit_get('private-address'),
            'port': 8080,
        }

components = [
    {
        'name': 'WordPress',
        'provides': [HttpRelation],
        'requires': [
            helpers.config_not_default('password'),
            helpers.Relation(
                relation_name = 'db',
                required_keys = ['host', 'user', 'password', 'database'],
            ),
        ],
        'callbacks': [
            actions.install_frontend,
            helpers.template(source='wp-config.php.j2',
                             target=os.path.join(WP_INSTALL_DIR, 'wp-config.php'))
            helpers.template(source='wordpress.upstart.j2',
                             target='/etc/init/wordpress'),
            helpers.service_restart('wordpress'),
            helpers.open_ports(8080),
        ],
        'cleanup': [
            helpers.close_ports(8080),
            helpers.service_stop('wordpress'),
        ],
    },
]

def manage():
    Manager(components).manage()

Each time a hook is fired, the conditions will be checked (in this case, that the password has been set, and that the db relation is connected and has all of the required keys) and, if met, the appropriate actions taken (correct front-end installed, config files written / updated, and the Upstart job (re)started).

Relation Data Providers

If your charm provides any relations for other charms to use, this is where you will define the data that is sent to them. You will almost certainly want to use the charmhelpers.core.charmframework.helpers.Relation base class, and then define a provide() method to generate or collect the data to be sent.

A more complicated example, which relies on the name of the remote service might be:

class DatabaseRelation(Relation):
    relation_name = 'db'

    def provide(self, remote_service, all_ready):
        if not all_ready:
            return None  # not yet ready to provide our service
        if database_exists(remote_service):
            password = get_user_password(remote_service)
        else:
            password = create_user(remote_service)
            create_database(remote_service)
        return {
            'host': hookenv.unit_get('private-address'),
            'user': remote_service,
            'password': password,
            'database': remote_service,
        }

Alternatively, an example that involves bi-directional data exchange might be:

class NamedDatabaseRelation(Relation):
    relation_name = 'named-db'
    required_keys = ['database']

    def provide(self, remote_service, all_ready):
        if not all_ready:
            return None  # not yet ready to provide our service
        data = self.filtered_data(remote_service).values()[0]
        database = data['database']
        if not database_exists(database):
            create_database(database)
        password = get_or_create_user(remote_service)
        return {
            'host': hookenv.unit_get('private-address'),
            'user': remote_service,
            'password': password,
            'database': database,
        }

Requirement Predicates

Chances are, your charm depends on some external source of data before it can properly configure itself. The most common dependencies are on user config options, and relations to other charms.

Requirement predicates are responsible for checking those dependencies and verifying that they are satisfied so that the charm can proceed with its setup. Again, you can use the Relation class, or any of the other predicate helpers defined in charmhelpers.core.charmframework.helpers to make these predicates easy.

(Note that Relation subclasses will generally cover both sides of the relation, provides and requires, so that all of the logic for a relation interface protocol is encapsulated in a single class. These classes can then be re-used among multiple Charms, and participating in a relation becomes as easy as using a pre-defined class.)

Ready Callbacks

These are just simple callbacks that will be triggered once all of the requires dependencies are met. It is highly recommended that you split these into logical, easily unit-testable chunks.

The charmhelpers.core.charmframework.helpers module has helpers for common actions, such as opening ports, rendering templates, and restarting services.

The callbacks can use any of the standard charm-helpers methods for performing their work, but the recommended way to access relation data for relations in the requires dependencies is via any_ready_unit() or all_ready_units() helpers, which use the data stored in charmhelpers.core.unitdata, since some relations may have units connected which do not yet satisfy the requirements to participate with the charm.

Cleanup Callbacks

These callbacks are triggered if the charm is being stopped, or if any of the requires dependencies change such that the requirements can no longer be met.

The charmhelpers.core.charmframework.helpers module has helpers for common actions, such as closing ports, and stopping services.

Conclusion

By using this framework, it is easy to see what the preconditions for the charm are, and there is never a concern about things being in a partially configured state. As a charm author, you can focus on what is important to you: what data is mandatory, what is optional, and what actions should be taken once the requirements are met.