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

Subscribers

People subscribed via source and target branches