Charm anatomy¶
Overview¶
The new OpenStack charms (charms written in 2016 onwards) are written using the reactive framework in Python. An introduction on the reactive framework and building charms from layers can be found in the Getting Started with charm development . This guide covers only the new reactive charms.
Configuration Files¶
layers.yaml¶
The src/layers.yaml file defines what layers and interfaces will be imported and included in the charm when the charm is built. See the Openstack Layers section and OpenStack Interfaces section below. If additional interfaces or layers add them to the includes list within src/layers.yaml.
Below is an example of the layers.yaml for an OpenStack API charm which has a relation with MongoDB:
includes: ['layer:openstack-api', 'interface:mongodb']
options:
basic:
use_venv: True
include_system_packages: True
When the charm is built the openstack-api layer and mongodb interface will be included in the built charm. The charm will run in a virtual env with system packages exposed in that virtual env. See the Layer Configuration section in Basic Layer README for more details of the configurable options in a layers.yaml
config.yaml¶
The charm authors guide contains a section on the config.yaml and is a good place to start. The config.yaml of the built charm is constructed from each layer that contains a config.yaml.
metadata.yaml¶
The charm metadata.yaml describes the charm and how it relates to other charms. This is also constructed from each layer that defines a metadata.yaml
OpenStack Layers¶
Basic Layer¶
The Basic Layer is the base layer for all charms built using layers. It provides all of the standard Juju hooks and runs the charms.reactive.main loop for them. It also bootstraps the charm-helpers and charms.reactive libraries and all of their dependencies for use by the charm.
OpenStack Layer¶
The Openstack Layer provides the base OpenStack configuration options, templates, template fragments and dependencies for authoring OpenStack Charms. Typically this layer is used for subordinate charms. The openstack-api or openstack-principle layers are probably more appropriate for principle charms and both of those layers inherit this one.
This layer includes a wheelhouse to pull in charms.openstack . See charms.openstack for more details.
Openstack Principle Layer¶
The Openstack Principle Layer provides the base layer for OpenStack charms that are intended for use as principle (rather than subordinate)
Openstack API Layer¶
The Openstack API Layer provides the base layer for OpenStack charms that deploy API services, and provides all of the core functionality for:
HA (using the hacluster charm)
SSL (using configuration options or keystone for certificates)
Juju 2.0 network space support for API endpoints
Configuration based network binding of API endpoints
It also pulls in interfaces mysql-shared, rabbitmq and keystone which are common to API charms.
OpenStack Interfaces¶
Interfaces define the data exchange between each of the charms. A list of all available interfaces is available here. A list of OpenStack specific interfaces can be found here
The interfaces a charm needs are defined in the layers.yaml. Below is a list of the typical interfaces needed by different OpenStack charm types:
API Charm
Neutron SDN Plugin
Neutron ODL Based SDN Plugin
Neutron API Plugin
charms.openstack¶
The charms.openstack python module provides helpers for building layered, reactive OpenStack charms. It is installed by the OpenStack Layer .
Defining the Charm¶
The charm is defined be extending the OpenStackCharm or OpenStackCharmAPI base classes in src/lib/charm/openstack/new_charm_name.py and overriding the class attributes as needed.
For example to define a charm for a service called ‘new-service’:
import charms_openstack.charm
class NewServiceCharm(charms_openstack.charm.OpenStackCharm):
# The name of the charm (for printing, etc.)
name = 'new-service'
# List of packages to install
packages = ['glance-common']
# The list of required services that are checked for assess_status
# e.g. required_relations = ['identity-service', 'shared-db']
required_relations = ['keystone']
# A dictionary of:
# {
# 'config.file': ['list', 'of', 'services', 'to', 'restart'],
# 'config2.file': ['more', 'services'],
# }
# The files that for the keys of the dict are monitored and if the file
# changes the corresponding services are restarted
restart_map = {
'/etc/new-svc/new-svc.conf': ['new-charm-svc']}
# first_release = this is the first release in which this charm works
release = 'icehouse'
def configure_foo(self):
...
The charm definition above can also define methods, like configure_foo, that the charm handlers can call to run charm specific code.
Reacting to Events¶
Reactive charms react to events. These events could be raised by interfaces or by other handlers. A number of event handlers are added by default by the charms.openstack module. For example, an install handler runs by default and will install the packages which were listed in NewServiceCharm.packages. Once complete the ‘charm.installed’ state is raised. The charms handlers specific to the new charm are defined in src/reactive/new_charm_name_handlers.py
For example, once the packages are installed it is likely that additional configuration is needed e.g. rendering config, configuring bridges or updating remote services via their interfaces. To perform an action once the initial package installation has been done a handler needs to be added to listen for the charm.installed event. To do this edit src/reactive/new_charm_name_handlers.py and add the reactive handler:
@reactive.when('charm.installed')
def configure_foo():
with charm.provide_charm_instance() as new_charm:
new_charm.configure_foo()
If configure_foo() should only be run once then the handler can emit a new state and the running of configure_foo gated on the state not being present e.g.
@reactive.when_not('foo.configured')
@reactive.when('charm.installed')
def configure_foo():
with charm.provide_charm_instance() as new_charm:
new_charm.configure_foo()
reactive.set_state('foo.configured')
File Templates¶
Most charms need to write a configuration file from a template. The templates are stored in src/templates see Templates Directory for more details. The context used to populate the template has a number of namespaces which are populated from different sources. Below outlines those namespaces.
Note
Hyphens are always automatically converted to underscores in the template context.
Template properties from Interfaces¶
By default some interfaces are automatically allocated a namespace within the template context. Those namespaces are also automatically populated with some options directly from the interface. For example if a charm is related to Keystone’s keystone interface then a number of service_ variables are set in the identity_service namespace. So, charm template could contain the following to access those variables:
[keystone_authtoken]
www_authenticate_uri = {{ identity_service.service_protocol }}://{{ identity_service.service_host }}:{{ identity_service.service_port }}
auth_url = {{ identity_service.auth_protocol }}://{{ identity_service.auth_host }}:{{ identity_service.auth_port }}
See the auto_accessors list in charm-interface-keystone for a complete list
However, most interface data is accessed via Adapters…
Template properties from Adapters¶
Adapters are used to take the data from an interface and create new variables
in the template context. For example the RabbitMQRelationAdapter (which can
be found in the adapters.py
from charms.openstack.) adds an ssl_ca_file variable to the amqp
namespace. This setting is really independent of the interface with rabbit but
should be consistent across the OpenStack deployment. This variable can then
be accessed in the same way as the rest of the amqp setting {{amqp.ssl_ca_file }}
Template properties from user config¶
The settings exposed to the user via the config.yaml are added to the
options namespace. The value the user has set for option foo can be
retrieved inside a template by including {{ options.foo }}
Template properties added to user config¶
It is useful to be able to set a property based on examining multiple config options or examining other aspects of the runtime system. The charms_openstack.adapters.config_property decorator can be used to achieve this. In the example below if the user has set the boolean config option angry to True and set the radiation string config option to gamma then the hulk_mode property is set to True.
@charms_openstack.adapters.config_property
def hulk_mode(config):
if config.angry and config.radiation =='gamma':
return True
else:
return False
This can be accessed in the templates with {{ options.hulk_mode }}
Template properties added to an Adapter¶
To be able to set a property based on the settings retrieved from an interface. In the example below the charm sets a pipeline based on the Keystone API version advertised by the keystone interface,
@charms_openstack.adapters.adapter_property('identity_service')
def charm_pipeline(keystone):
return {
"2": "cors keystone_authtoken context apiapp",
"3": "cors keystone_v3_authtoken context apiapp",
"none": "cors unauthenticated-context apiapp"
}[keystone.api_version]
This can be accessed in the templates with {{ identity_service.charm_pipeline }}
Templates Directory¶
Templates are loaded from several places in the following order:
From the most recent OS release-specific template dir (if one exists)
Working back through the template directories for each earlier OpenStack Release
The base templates_dir
For the example above, ‘templates’ contains the following structure:
templates/nova.conf
templates/api-paste.ini
templates/kilo/api-paste.ini
templates/newton/api-paste.ini
If the charm is deploying the Newton release, it first searches the newton directory for nova.conf, then the templates dir. So templates/nova.conf will be used.
When writing api-paste.ini, it will find the template in the newton directory.
However if Liberty was being installed then the charm would fall back to the kilo template for api-paste.ini since there is no Liberty specific version.
Rendering a Template¶
Rendering the templates does not usually make sense until all the interfaces
that are going to supply the template context with data are ready and
available. The @reactive.when
decorator not only ensures that the wrapped
method is not run until the interface is ready, it also passes an instance of
the interface to the method it is wrapping. These interfaces can then be passed
to the render_with_interfaces class which looks after finding the templates
and rendering them. render_with_interfaces decides which files need rendering
by examining the keys of the restart_map dict which was specified as part of
the charm class. Taking all this together results in a handler like this:
@reactive.when('shared-db.available')
@reactive.when('identity-service.available')
@reactive.when('amqp.available')
def render_config(*args):
with charm.provide_charm_instance() as new_charm:
new_charm.render_with_interfaces(args)
new_charm.assess_status()
Sending data via an Interface¶
Some interfaces are used to send as well as receive data. The interface will expose a method for sending data to a remote application if it is supported. For example the neutron-plugin interface can be used to send configuration to the principle charm.
The handler below waits for the neutron-plugin relation with the principle to be complete at which point the neutron-plugin.connected state will be set which will fire this trigger. An instance of the interface is passed by the decorator to the configure_neutron_plugin method. This is in turn passed to the configure_neutron_plugin method in the charm class.
@reactive.when('neutron-plugin.connected')
def configure_neutron_plugin(neutron_plugin):
with charm.provide_charm_instance() as new_charm:
new_charm.configure_neutron_plugin(neutron_plugin)
In the charm class the instance of the interface is used to update the principle
def configure_neutron_plugin(self, neutron_plugin):
neutron_plugin.configure_plugin(
plugin='mysdn',
config={
"nova-compute": {
"/etc/nova/nova.conf": {
"sections": {
'DEFAULT': [
('firewall_driver',
'nova.virt.firewall.'
'NoopFirewallDriver'),
('libvirt_vif_driver',
'nova.virt.libvirt.vif.'
'LibvirtGenericVIFDriver'),
('security_group_api', 'neutron'),
],
}
}
}
})
On receiving this data from the neutron_plugin relation the principle will add the requested config into /etc/nova/nova.conf
Note
The amqp, shared-db and identity-service interfaces are automatically updated so there is no need to add code for them unless a bespoke configuration is needed.
Displaying Charm Status¶
The charm can declare what state it is in and this status is displayed to the
user via juju status. By default the charm code will look for the
required_relations
attribute of the charm class. required_relations
is
a list of interfaces. e.g. for an API charm …
required_relations = ['shared-db', 'amqp', 'identity-service']
The in built assess_status()
method will check that each interface has
raised the {relation}.available state. If the relation is missing altogether
or if the relation has yet to raise the {relation}.available state then a
message is returned via juju status