Merge lp:~johnsca/charm-helpers/services-docs into lp:charm-helpers

Proposed by Cory Johns
Status: Merged
Merged at revision: 197
Proposed branch: lp:~johnsca/charm-helpers/services-docs
Merge into: lp:charm-helpers
Diff against target: 223 lines (+174/-9)
2 files modified
charmhelpers/core/services/base.py (+14/-9)
docs/examples/services.rst (+160/-0)
To merge this branch: bzr merge lp:~johnsca/charm-helpers/services-docs
Reviewer Review Type Date Requested Status
Charles Butler (community) Approve
Benjamin Saller (community) Approve
charmers Pending
Review via email: mp+231235@code.launchpad.net

Description of the change

Added more detailed narrative docs for services framework.

To post a comment you must log in.
Revision history for this message
Benjamin Saller (bcsaller) wrote :

This is a clear improvement. LGTM, thanks!

review: Approve
Revision history for this message
Charles Butler (lazypower) wrote :

+1 LGTM - thank you for the documentation cory_fu! this is a great submission.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'charmhelpers/core/services/base.py'
--- charmhelpers/core/services/base.py 2014-08-05 21:28:01 +0000
+++ charmhelpers/core/services/base.py 2014-08-18 17:27:37 +0000
@@ -17,20 +17,13 @@
17 """17 """
18 Register a list of services, given their definitions.18 Register a list of services, given their definitions.
1919
20 Traditional charm authoring is focused on implementing hooks. That is,
21 the charm author is thinking in terms of "What hook am I handling; what
22 does this hook need to do?" However, in most cases, the real question
23 should be "Do I have the information I need to configure and start this
24 piece of software and, if so, what are the steps for doing so?" The
25 ServiceManager framework tries to bring the focus to the data and the
26 setup tasks, in the most declarative way possible.
27
28 Service definitions are dicts in the following formats (all keys except20 Service definitions are dicts in the following formats (all keys except
29 'service' are optional)::21 'service' are optional)::
3022
31 {23 {
32 "service": <service name>,24 "service": <service name>,
33 "required_data": <list of required data contexts>,25 "required_data": <list of required data contexts>,
26 "provided_data": <list of provided data contexts>,
34 "data_ready": <one or more callbacks>,27 "data_ready": <one or more callbacks>,
35 "data_lost": <one or more callbacks>,28 "data_lost": <one or more callbacks>,
36 "start": <one or more callbacks>,29 "start": <one or more callbacks>,
@@ -44,6 +37,10 @@
44 of 'data_ready' and 'start' callbacks executed. See `is_ready()` for more37 of 'data_ready' and 'start' callbacks executed. See `is_ready()` for more
45 information.38 information.
4639
40 The 'provided_data' list should contain relation data providers, most likely
41 a subclass of :class:`charmhelpers.core.services.helpers.RelationContext`,
42 that will indicate a set of data to set on a given relation.
43
47 The 'data_ready' value should be either a single callback, or a list of44 The 'data_ready' value should be either a single callback, or a list of
48 callbacks, to be called when all items in 'required_data' pass `is_ready()`.45 callbacks, to be called when all items in 'required_data' pass `is_ready()`.
49 Each callback will be called with the service name as the only parameter.46 Each callback will be called with the service name as the only parameter.
@@ -123,12 +120,20 @@
123 self.reconfigure_services()120 self.reconfigure_services()
124121
125 def provide_data(self):122 def provide_data(self):
123 """
124 Set the relation data for each provider in the ``provided_data`` list.
125
126 A provider must have a `name` attribute, which indicates which relation
127 to set data on, and a `provide_data()` method, which returns a dict of
128 data to set.
129 """
126 hook_name = hookenv.hook_name()130 hook_name = hookenv.hook_name()
127 for service in self.services.values():131 for service in self.services.values():
128 for provider in service.get('provided_data', []):132 for provider in service.get('provided_data', []):
129 if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):133 if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
130 data = provider.provide_data()134 data = provider.provide_data()
131 if provider._is_ready(data):135 _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data
136 if _ready:
132 hookenv.relation_set(None, data)137 hookenv.relation_set(None, data)
133138
134 def reconfigure_services(self, *service_names):139 def reconfigure_services(self, *service_names):
135140
=== added file 'docs/examples/services.rst'
--- docs/examples/services.rst 1970-01-01 00:00:00 +0000
+++ docs/examples/services.rst 2014-08-18 17:27:37 +0000
@@ -0,0 +1,160 @@
1Managing Charms with the Services Framework
2===========================================
3
4Traditional charm authoring is focused on implementing hooks. That is,
5the charm author is thinking in terms of "What hook am I handling; what
6does this hook need to do?" However, in most cases, the real question
7should be "Do I have the information I need to configure and start this
8piece of software and, if so, what are the steps for doing so?" The
9services framework tries to bring the focus to the data and the
10setup tasks, in the most declarative way possible.
11
12
13Hooks as Data Sources for Service Definitions
14---------------------------------------------
15
16While the ``install``, ``start``, and ``stop`` hooks clearly represent
17state transitions, all of the other hooks are really notifications of
18changes in data from external sources, such as config data values in
19the case of ``config-changed`` or relation data for any of the
20``*-relation-*`` hooks. Moreover, many charms that rely on external
21data from config options or relations find themselves needing some
22piece of external data before they can even configure and start anything,
23and so the ``start`` hook loses its semantic usefulness.
24
25If data is required from multiple sources, it even becomes impossible to
26know which hook will be executing when all required data is available.
27(E.g., which relation will be the last to execute; will the required
28config option be set before or after all of the relations are available?)
29One common solution to this problem is to create "flag files" to track
30whether a given bit of data has been observed, but this can get cluttered
31quickly and is difficult to understand what conditions lead to which actions.
32
33When using the services framework, all hooks other than ``install``
34are handled by a single call to :meth:`manager.manage() <charmhelpers.core.services.base.ServiceManager.manage>`.
35This can be done with symlinks, or by having a ``definitions.py`` file
36containing the service defintions, and every hook can be reduced to::
37
38 #!/bin/env python
39 from charmhelpers.core.services import ServiceManager
40 from definitions import service_definitions
41 ServiceManager(service_definitions).manage()
42
43So, what magic goes into ``definitions.py``?
44
45
46Service Definitions Overview
47----------------------------
48
49The format of service definitions are fully documented in
50:class:`~charmhelpers.core.services.base.ServiceManager`, but most commonly
51will consist of one or more dictionaries containing four items: the name of
52a service being managed, the list of data contexts required before the service
53can be configured and started, the list of actions to take when the data
54requirements are satisfied, and list of ports to open. The service name
55generally maps to an Upstart job, the required data contexts are ``dict``
56or ``dict``-like structures that contain the data once available (usually
57subclasses of :class:`~charmhelpers.core.services.helpers.RelationContext`
58or wrappers around :func:`charmhelpers.core.hookenv.config`), and the actions
59are just callbacks that are passed the service name for which they are executing
60(or a subclass of :class:`~charmhelpers.core.services.base.ManagerCallback`
61for more complex cases).
62
63An example service definition might be::
64
65 service_definitions = [
66 {
67 'service': 'wordpress',
68 'ports': [80],
69 'required_data': [config(), MySQLRelation()],
70 'data_ready': [
71 actions.install_frontend,
72 services.render_template(source='wp-config.php.j2',
73 target=os.path.join(WP_INSTALL_DIR, 'wp-config.php'))
74 services.render_template(source='wordpress.upstart.j2',
75 target='/etc/init/wordpress'),
76 ],
77 },
78 ]
79
80Each time a hook is fired, the conditions will be checked (in this case, just
81that MySQL is available) and, if met, the appropriate actions taken (correct
82front-end installed, config files written / updated, and the Upstart job
83(re)started, implicitly).
84
85
86Required Data Contexts
87----------------------
88
89Required data contexts are, at the most basic level, are just dictionaries,
90and if they evaluate as True (e.g., if the contain data), their condition is
91considered to be met. A simple sentinal could just be a function that returns
92data if available or an empty ``dict`` otherwise.
93
94For the common case of gathering data from relations, the
95:class:`~charmhelpers.core.services.helpers.RelationContext` base class gathers
96data from a named relation and checks for a set of required keys to be present
97and set on the relation before considering that relation complete. For example,
98a basic MySQL context might be::
99
100 class MySQLRelation(RelationContext):
101 name = 'db'
102 interface = 'mysql'
103 required_keys = ['host', 'user', 'password', 'database']
104
105Because there could potentially be multiple units on a given relation, and
106to prevent conflicts when the data contexts are merged to be sent to templates
107(see below), the data for a ``RelationContext`` is nested in the following way::
108
109 relation[relation.name][unit_number][relation_key]
110
111For example, to get the host of the first MySQL unit (``mysql/0``)::
112
113 mysql = MySQLRelation()
114 unit_0_host = mysql[mysql.name][0]['host']
115
116Note that only units that have set values for all of the required keys are
117included in the list, and if no units have set all of the required keys,
118instantiating the ``RelationContext`` will result in an empty list.
119
120
121Data-Ready Actions
122------------------
123
124When a hook is triggered and all of the ``required_data`` contexts are complete,
125the list of "data ready" actions are executed. These callbacks are passed
126the service name from the ``service`` key of the service definition for which
127they are running, and are responsible for (re)configuring the service
128according to the required data.
129
130The most common action should be to render a config file from a template.
131The :class:`render_template <charmhelpers.core.services.helpers.TemplateCallback>`
132helper will merge all of the ``required_data`` contexts and render a
133`Jinja2 <http://jinja.pocoo.org/>`_ template with the combined data. For
134example, to render a list of DSNs for units on the db relation, the
135template should include::
136
137 databases: [
138 {% for unit in db %}
139 "mysql://{{unit['user']}}:{{unit['password']}}@{{unit['host']}}/{{unit['database']}}",
140 {% endfor %}
141 ]
142
143Note that the actions need to be idempotent, since they will all be re-run
144if something about the charm changes (that is, if a hook is triggered). That
145is why rendering a template is preferred to editing a file via regular expression
146substitutions.
147
148Also note that the actions are not responsible for starting the service; there
149are separate ``start`` and ``stop`` options that default to starting and stopping
150an Upstart service with the name given by the ``service`` value.
151
152
153Conclusion
154----------
155
156By using this framework, it is easy to see what the preconditions for the charm
157are, and there is never a concern about things being in a partially configured
158state. As a charm author, you can focus on what is important to you: what
159data is mandatory, what is optional, and what actions should be taken once
160the requirements are met.

Subscribers

People subscribed via source and target branches