.. _charm_anatomy:
=============
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`:
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:
.. code:: yaml
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`:
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`:
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 are will 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`:
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 defines in the `layers.yaml`_. Below is a list
of the typical interfaces needed by different OpenStack charm types:
**API Charm**
- `mysql-shared `__
- `rabbitmq `__
- `keystone `__
**Neutron SDN Plugin**
- `neutron-plugin `__
- `service-control `__
**Neutron ODL Based SDN Plugin**
- `neutron-plugin `__
- `service-control `__
- `ovsdb-manager `__
- `odl-controller-api `__
**Neutron API Plugin**
- `neutron-plugin-api-subordinate `__
- `service-control `__
.. _`charms.openstack`:
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':
.. code:: python
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:
.. code:: python
@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.
.. code:: python
@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::
Hypens 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:
.. code:: python
[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.
.. code:: python
@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,
.. code:: python
@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 Directory
~~~~~~~~~~~~~~~~~~~
Template 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:
.. code:: python
@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.
.. code:: python
@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
.. code:: python
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 ...
.. code:: python
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``