Merge lp:~johnsca/charms/trusty/cf-cloud-controller/services-callback-fu into lp:~cf-charmers/charms/trusty/cf-cloud-controller/trunk

Proposed by Cory Johns
Status: Merged
Merged at revision: 29
Proposed branch: lp:~johnsca/charms/trusty/cf-cloud-controller/services-callback-fu
Merge into: lp:~cf-charmers/charms/trusty/cf-cloud-controller/trunk
Diff against target: 950 lines (+493/-337)
12 files modified
hooks/charmhelpers/contrib/cloudfoundry/contexts.py (+41/-32)
hooks/charmhelpers/core/host.py (+5/-0)
hooks/charmhelpers/core/services.py (+316/-79)
hooks/charmhelpers/core/templating.py (+38/-145)
hooks/config-changed (+9/-0)
hooks/config.py (+39/-0)
hooks/db-relation-changed (+9/-0)
hooks/hooks.py (+0/-81)
hooks/nats-relation-changed (+9/-0)
hooks/router-relation-changed (+9/-0)
hooks/stop (+9/-0)
hooks/upgrade-charm (+9/-0)
To merge this branch: bzr merge lp:~johnsca/charms/trusty/cf-cloud-controller/services-callback-fu
Reviewer Review Type Date Requested Status
Cloud Foundry Charmers Pending
Review via email: mp+220741@code.launchpad.net

Description of the change

Fixed relation ordering issue

Fixed an relation hook ordering dependency by refactoring to use
callbacks in charmhelpers.core.services

https://codereview.appspot.com/100680044/

To post a comment you must log in.
Revision history for this message
Cory Johns (johnsca) wrote :

Reviewers: mp+220741_code.launchpad.net,

Message:
Please take a look.

Description:
Fixed relation ordering issue

Fixed an relation hook ordering dependency by refactoring to use
callbacks in charmhelpers.core.services

https://code.launchpad.net/~johnsca/charms/trusty/cf-cloud-controller/services-callback-fu/+merge/220741

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/100680044/

Affected files (+134, -112 lines):
   A [revision details]
   M hooks/charmhelpers/core/host.py
   M hooks/charmhelpers/core/services.py
   M hooks/charmhelpers/core/templating.py
   M hooks/hooks.py

Revision history for this message
Benjamin Saller (bcsaller) wrote :

Thanks I like this, I'll approve it pending the answer to the one
question

https://codereview.appspot.com/100680044/diff/1/hooks/hooks.py
File hooks/hooks.py (right):

https://codereview.appspot.com/100680044/diff/1/hooks/hooks.py#newcode18
hooks/hooks.py:18: def register():
why the containing method? it seems fine to register with hooks on
import

https://codereview.appspot.com/100680044/diff/1/hooks/hooks.py#newcode41
hooks/hooks.py:41:
'router-relation-changed')(services.reconfigure_services)
The other pattern is more familiar but I think we could get people used
to this and it has the advantage of being very concise. :)

https://codereview.appspot.com/100680044/

30. By Cory Johns

Use refactored services API and remove symlink hooks

Revision history for this message
Cory Johns (johnsca) wrote :
Revision history for this message
Whit Morriss (whitmo) wrote :

LGTM. nice and concise.

https://codereview.appspot.com/100680044/diff/20001/hooks/charmhelpers/core/services.py
File hooks/charmhelpers/core/services.py (left):

https://codereview.appspot.com/100680044/diff/20001/hooks/charmhelpers/core/services.py#oldcode5
hooks/charmhelpers/core/services.py:5: SERVICES = {}
As discussed with Cory: we are abusing module scope and globals to
emulate object behavior. I feel the code will age and extend better if
we sacrificed a tiny bit of api convenience and used idiomatic python
classes here.

https://codereview.appspot.com/100680044/

31. By Cory Johns

Updated charm-helpers and switched to class API for ServiceManager

Revision history for this message
Cory Johns (johnsca) wrote :
Revision history for this message
Benjamin Saller (bcsaller) wrote :

So this is reviewed in the context of a charm, but its nice to see the
top level singleton thing play out.

I have some more bikeshedding suggestions, but they are only that. Feel
free to land this and the helpers changes. They

LGTM +1

Thanks

https://codereview.appspot.com/100680044/diff/40001/hooks/charmhelpers/core/services.py
File hooks/charmhelpers/core/services.py (right):

https://codereview.appspot.com/100680044/diff/40001/hooks/charmhelpers/core/services.py#newcode9
hooks/charmhelpers/core/services.py:9: class ServiceManager(object):
Even though we only use this as a Singleton this is almost certainly the
proper way to evolve the code. Better to do this now (because its a
minor api change) than later, thanks.

https://codereview.appspot.com/100680044/diff/40001/hooks/charmhelpers/core/services.py#newcode21
hooks/charmhelpers/core/services.py:21: "data_lost": <one or more
callbacks>,
Not sure I love data_ready/data_lost. Still I feel like I'm bike
shedding and whit already reviewed this and was ok with the naming so
I'll pass if you don't like these better

'requires': [],
'complete': []
'incomplete': []

another option is to keep with the naming we use in relations though
I'll admit 'joined' is a little hard to understand in this context, so
maybe

'requires', 'changed', 'broken'

I do think 'broken' is a good name, but it needs something that fits
conceptually with it. At this point though I do think what you have is
understandable and nicely documented.

https://codereview.appspot.com/100680044/diff/40001/hooks/config.py
File hooks/config.py (right):

https://codereview.appspot.com/100680044/diff/40001/hooks/config.py#newcode8
hooks/config.py:8:
Yeah, not bad.

https://codereview.appspot.com/100680044/

Revision history for this message
Cory Johns (johnsca) wrote :

On 2014/05/27 18:06:56, benjamin.saller wrote:
> Not sure I love data_ready/data_lost. Still I feel like I'm bike
shedding and
> whit already reviewed this and was ok with the naming so I'll pass if
you don't
> like these better

> 'requires': [],
> 'complete': []
> 'incomplete': []

> another option is to keep with the naming we use in relations though
I'll admit
> 'joined' is a little hard to understand in this context, so maybe

> 'requires', 'changed', 'broken'

> I do think 'broken' is a good name, but it needs something that fits
> conceptually with it. At this point though I do think what you have is
> understandable and nicely documented.

Yeah, we talked about nomenclature quite a bit on Friday. Whit's point
was that our "dependencies" list was overloaded and actually serves two
purposes:

   1) Define what the service needs to operate that it can't get for
itself (relations, config data, etc)
   2) Collect all of that information into a unified view

Naming it "dependencies" or "requirements" makes that second aspect
non-obvious, and it would be best for the API to be, as much as
possible, self-documenting. Given that, "required_data" was the best we
could come up with, as it conveys both that this is a list of required
things, and that those things provide data somehow. This also plays
into the ability to, instead of having a semi-opaque StaticContext
class, you can now just drop a plain dict (or the results of config())
in there directly and it will do what you expect.

Then, I also realized that I wanted to separate the "ready" (or
"complete," or whatever) event from the "start" event, so that the
charms can just worry about defining what happens between everything the
service needs being available and spinning up the service itself,
without having to worry about the details of spinning it up by manually
specifying host.service_start every time, since 90% of charms should be
using Upstart or some other SysV-compatible service manager.

So, then we end up with the events of interest:

   * All of the requirements have been satisfied and the data is
available to configure and start the service
   * Something changed, and we no longer have complete information for
running the service

The names we chose, "data_ready" and "data_lost" seemed to capture those
ideas while matching up well to "required_data." Additionally,
"data_lost" specially calls out the fact that it doesn't need to be
triggered in the case that we're still working to get set up and we've
never had the service set up, so nothing needs to be done to tear it
down.

I'm not opposed to "complete" and "incomplete," or "complete" and
"broken." But I also think that it will not be difficult to change the
keys later in a backwards-compatible way (since we just look for one of
a set of keys, and it would be all encapsulated in fire_event).

So, my inclination is to leave it as-is until / if we get more feedback
that "required_data," "data_ready," and "data_lost" are confusing.

https://codereview.appspot.com/100680044/

32. By Cory Johns

Updated charm-helpers and removed implicit target from templates

Revision history for this message
Cory Johns (johnsca) wrote :
Revision history for this message
Cory Johns (johnsca) wrote :
Revision history for this message
Cory Johns (johnsca) wrote :

*** Submitted:

Fixed relation ordering issue

Fixed an relation hook ordering dependency by refactoring to use
callbacks in charmhelpers.core.services

R=benjamin.saller, whit.morriss
CC=
https://codereview.appspot.com/100680044

https://codereview.appspot.com/100680044/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'hooks/charmhelpers/contrib/cloudfoundry/contexts.py'
--- hooks/charmhelpers/contrib/cloudfoundry/contexts.py 2014-05-16 23:33:25 +0000
+++ hooks/charmhelpers/contrib/cloudfoundry/contexts.py 2014-05-29 17:26:14 +0000
@@ -1,55 +1,64 @@
1import os1import os
22import yaml
3from charmhelpers.core.templating import (3
4 ContextGenerator,4from charmhelpers.core.services import RelationContext
5 RelationContext,5
6 StorableContext,6
7)7class StoredContext(dict):
88 """
99 A data context that always returns the data that it was first created with.
10# Stores `config_data` hash into yaml file with `file_name` as a name10 """
11# if `file_name` already exists, then it loads data from `file_name`.
12class StoredContext(ContextGenerator, StorableContext):
13 def __init__(self, file_name, config_data):11 def __init__(self, file_name, config_data):
12 """
13 If the file exists, populate `self` with the data from the file.
14 Otherwise, populate with the given data and persist it to the file.
15 """
14 if os.path.exists(file_name):16 if os.path.exists(file_name):
15 self.data = self.read_context(file_name)17 self.update(self.read_context(file_name))
16 else:18 else:
17 self.store_context(file_name, config_data)19 self.store_context(file_name, config_data)
18 self.data = config_data20 self.update(config_data)
1921
20 def __call__(self):22 def store_context(self, file_name, config_data):
21 return self.data23 with open(file_name, 'w') as file_stream:
2224 yaml.dump(config_data, file_stream)
2325
24class NatsContext(RelationContext):26 def read_context(self, file_name):
27 with open(file_name, 'r') as file_stream:
28 data = yaml.load(file_stream)
29 if not data:
30 raise OSError("%s is empty" % file_name)
31 return data
32
33
34class NatsRelation(RelationContext):
25 interface = 'nats'35 interface = 'nats'
26 required_keys = ['nats_port', 'nats_address', 'nats_user', 'nats_password']36 required_keys = ['nats_port', 'nats_address', 'nats_user', 'nats_password']
2737
2838
29class MysqlDSNContext(RelationContext):39class MysqlRelation(RelationContext):
30 interface = 'db'40 interface = 'db'
31 required_keys = ['user', 'password', 'host', 'database']41 required_keys = ['user', 'password', 'host', 'database']
32 dsn_template = "mysql2://{user}:{password}@{host}:{port}/{database}"42 dsn_template = "mysql2://{user}:{password}@{host}:{port}/{database}"
3343
34 def __call__(self):44 def get_data(self):
35 ctx = RelationContext.__call__(self)45 RelationContext.get_data(self)
36 if ctx:46 if self.is_ready():
37 if 'port' not in ctx:47 if 'port' not in self['db']:
38 ctx['db']['port'] = '3306'48 self['db']['port'] = '3306'
39 ctx['db']['dsn'] = self.dsn_template.format(**ctx['db'])49 self['db']['dsn'] = self.dsn_template.format(**self['db'])
40 return ctx50
4151
4252class RouterRelation(RelationContext):
43class RouterContext(RelationContext):
44 interface = 'router'53 interface = 'router'
45 required_keys = ['domain']54 required_keys = ['domain']
4655
4756
48class LogRouterContext(RelationContext):57class LogRouterRelation(RelationContext):
49 interface = 'logrouter'58 interface = 'logrouter'
50 required_keys = ['shared-secret', 'logrouter-address']59 required_keys = ['shared-secret', 'logrouter-address']
5160
5261
53class LoggregatorContext(RelationContext):62class LoggregatorRelation(RelationContext):
54 interface = 'loggregator'63 interface = 'loggregator'
55 required_keys = ['shared_secret', 'loggregator_address']64 required_keys = ['shared_secret', 'loggregator_address']
5665
=== modified file 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py 2014-05-20 19:43:29 +0000
+++ hooks/charmhelpers/core/host.py 2014-05-29 17:26:14 +0000
@@ -63,6 +63,11 @@
63 return False63 return False
6464
6565
66def service_available(service_name):
67 """Determine whether a system service is available"""
68 return service('status', service_name)
69
70
66def adduser(username, password=None, shell='/bin/bash', system_user=False):71def adduser(username, password=None, shell='/bin/bash', system_user=False):
67 """Add a user to the system"""72 """Add a user to the system"""
68 try:73 try:
6974
=== modified file 'hooks/charmhelpers/core/services.py'
--- hooks/charmhelpers/core/services.py 2014-05-16 22:44:17 +0000
+++ hooks/charmhelpers/core/services.py 2014-05-29 17:26:14 +0000
@@ -1,84 +1,321 @@
1import os
2import sys
3from collections import Iterable
1from charmhelpers.core import templating4from charmhelpers.core import templating
2from charmhelpers.core import host5from charmhelpers.core import host
36from charmhelpers.core import hookenv
47
5SERVICES = {}8
69class ServiceManager(object):
710 def __init__(self, services=None):
8def register(services, templates_dir=None):11 """
9 """12 Register a list of services, given their definitions.
10 Register a list of service configs.13
1114 Service definitions are dicts in the following formats (all keys except
12 Service Configs are dicts in the following formats:15 'service' are optional):
1316
14 {17 {
15 "service": <service name>,18 "service": <service name>,
16 "templates": [ {19 "required_data": <list of required data contexts>,
17 'target': <render target of template>,20 "data_ready": <one or more callbacks>,
18 'source': <optional name of template in passed in templates_dir>21 "data_lost": <one or more callbacks>,
19 'file_properties': <optional dict taking owner and octal mode>22 "start": <one or more callbacks>,
20 'contexts': [ context generators, see contexts.py ]23 "stop": <one or more callbacks>,
21 }24 "ports": <list of ports to manage>,
22 ] }25 }
2326
24 Either `source` or `target` must be provided.27 The 'required_data' list should contain dicts of required data (or
2528 dependency managers that act like dicts and know how to collect the data).
26 If 'source' is not provided for a template the templates_dir will29 Only when all items in the 'required_data' list are populated are the list
27 be consulted for ``basename(target).j2``.30 of 'data_ready' and 'start' callbacks executed. See `is_ready()` for more
2831 information.
29 If `target` is not provided, it will be assumed to be32
30 ``/etc/init/<service name>.conf``.33 The 'data_ready' value should be either a single callback, or a list of
31 """34 callbacks, to be called when all items in 'required_data' pass `is_ready()`.
32 for service in services:35 Each callback will be called with the service name as the only parameter.
33 service.setdefault('templates_dir', templates_dir)36 After these all of the 'data_ready' callbacks are called, the 'start'
34 SERVICES[service['service']] = service37 callbacks are fired.
3538
3639 The 'data_lost' value should be either a single callback, or a list of
37def reconfigure_services(restart=True):40 callbacks, to be called when a 'required_data' item no longer passes
38 """41 `is_ready()`. Each callback will be called with the service name as the
39 Update all files for all services and optionally restart them, if ready.42 only parameter. After these all of the 'data_ready' callbacks are called,
40 """43 the 'stop' callbacks are fired.
41 for service_name in SERVICES.keys():44
42 reconfigure_service(service_name, restart=restart)45 The 'start' value should be either a single callback, or a list of
4346 callbacks, to be called when starting the service, after the 'data_ready'
4447 callbacks are complete. Each callback will be called with the service
45def reconfigure_service(service_name, restart=True):48 name as the only parameter. This defaults to
46 """49 `[host.service_start, services.open_ports]`.
47 Update all files for a single service and optionally restart it, if ready.50
48 """51 The 'stop' value should be either a single callback, or a list of
49 service = SERVICES.get(service_name)52 callbacks, to be called when stopping the service. If the service is
50 if not service or service['service'] != service_name:53 being stopped because it no longer has all of its 'required_data', this
51 raise KeyError('Service not registered: %s' % service_name)54 will be called after all of the 'data_lost' callbacks are complete.
5255 Each callback will be called with the service name as the only parameter.
53 manager_type = service.get('type', UpstartService)56 This defaults to `[services.close_ports, host.service_stop]`.
54 manager_type(service).reconfigure(restart)57
5558 The 'ports' value should be a list of ports to manage. The default
5659 'start' handler will open the ports after the service is started,
57def stop_services():60 and the default 'stop' handler will close the ports prior to stopping
58 for service_name in SERVICES.keys():61 the service.
59 if host.service_running(service_name):62
60 host.service_stop(service_name)63
6164 Examples:
6265
63class ServiceTypeManager(object):66 The following registers an Upstart service called bingod that depends on
64 def __init__(self, service_definition):67 a mongodb relation and which runs a custom `db_migrate` function prior to
65 self.service_name = service_definition['service']68 restarting the service, and a Runit serivce called spadesd.
66 self.templates = service_definition['templates']69
67 self.templates_dir = service_definition['templates_dir']70 >>> manager = services.ServiceManager([
6871 ... {
69 def reconfigure(self, restart=True):72 ... 'service': 'bingod',
73 ... 'ports': [80, 443],
74 ... 'required_data': [MongoRelation(), config()],
75 ... 'data_ready': [
76 ... services.template(source='bingod.conf'),
77 ... services.template(source='bingod.ini',
78 ... target='/etc/bingod.ini',
79 ... owner='bingo', perms=0400),
80 ... ],
81 ... },
82 ... {
83 ... 'service': 'spadesd',
84 ... 'data_ready': services.template(source='spadesd_run.j2',
85 ... target='/etc/sv/spadesd/run',
86 ... perms=0555),
87 ... 'start': runit_start,
88 ... 'stop': runit_stop,
89 ... },
90 ... ])
91 ... manager.manage()
92 """
93 self.services = {}
94 for service in services or []:
95 service_name = service['service']
96 self.services[service_name] = service
97
98 def manage(self):
99 """
100 Handle the current hook by doing The Right Thing with the registered services.
101 """
102 hook_name = os.path.basename(sys.argv[0])
103 if hook_name == 'stop':
104 self.stop_services()
105 else:
106 self.reconfigure_services()
107
108 def reconfigure_services(self, *service_names):
109 """
110 Update all files for one or more registered services, and,
111 if ready, optionally restart them.
112
113 If no service names are given, reconfigures all registered services.
114 """
115 for service_name in service_names or self.services.keys():
116 if self.is_ready(service_name):
117 self.fire_event('data_ready', service_name)
118 self.fire_event('start', service_name, default=[
119 host.service_restart,
120 open_ports])
121 self.save_ready(service_name)
122 else:
123 if self.was_ready(service_name):
124 self.fire_event('data_lost', service_name)
125 self.fire_event('stop', service_name, default=[
126 close_ports,
127 host.service_stop])
128 self.save_lost(service_name)
129
130 def stop_services(self, *service_names):
131 """
132 Stop one or more registered services, by name.
133
134 If no service names are given, stops all registered services.
135 """
136 for service_name in service_names or self.services.keys():
137 self.fire_event('stop', service_name, default=[
138 close_ports,
139 host.service_stop])
140
141 def get_service(self, service_name):
142 """
143 Given the name of a registered service, return its service definition.
144 """
145 service = self.services.get(service_name)
146 if not service:
147 raise KeyError('Service not registered: %s' % service_name)
148 return service
149
150 def fire_event(self, event_name, service_name, default=None):
151 """
152 Fire a data_ready, data_lost, start, or stop event on a given service.
153 """
154 service = self.get_service(service_name)
155 callbacks = service.get(event_name, default)
156 if not callbacks:
157 return
158 if not isinstance(callbacks, Iterable):
159 callbacks = [callbacks]
160 for callback in callbacks:
161 if isinstance(callback, ManagerCallback):
162 callback(self, service_name, event_name)
163 else:
164 callback(service_name)
165
166 def is_ready(self, service_name):
167 """
168 Determine if a registered service is ready, by checking its 'required_data'.
169
170 A 'required_data' item can be any mapping type, and is considered ready
171 if `bool(item)` evaluates as True.
172 """
173 service = self.get_service(service_name)
174 reqs = service.get('required_data', [])
175 return all(bool(req) for req in reqs)
176
177 def save_ready(self, service_name):
178 """
179 Save an indicator that the given service is now data_ready.
180 """
181 ready_file = '{}/.ready.{}'.format(hookenv.charm_dir(), service_name)
182 with open(ready_file, 'a'):
183 pass
184
185 def save_lost(self, service_name):
186 """
187 Save an indicator that the given service is no longer data_ready.
188 """
189 ready_file = '{}/.ready.{}'.format(hookenv.charm_dir(), service_name)
190 if os.path.exists(ready_file):
191 os.remove(ready_file)
192
193 def was_ready(self, service_name):
194 """
195 Determine if the given service was previously data_ready.
196 """
197 ready_file = '{}/.ready.{}'.format(hookenv.charm_dir(), service_name)
198 return os.path.exists(ready_file)
199
200
201class RelationContext(dict):
202 """
203 Base class for a context generator that gets relation data from juju.
204
205 Subclasses must provide `interface`, which is the interface type of interest,
206 and `required_keys`, which is the set of keys required for the relation to
207 be considered complete. The first relation for the interface that is complete
208 will be used to populate the data for template.
209
210 The generated context will be namespaced under the interface type, to prevent
211 potential naming conflicts.
212 """
213 interface = None
214 required_keys = []
215
216 def __bool__(self):
217 """
218 Updates the data and returns True if all of the required_keys are available.
219 """
220 self.get_data()
221 return self.is_ready()
222
223 __nonzero__ = __bool__
224
225 def is_ready(self):
226 """
227 Returns True if all of the required_keys are available.
228 """
229 return set(self.get(self.interface, {}).keys()).issuperset(set(self.required_keys))
230
231 def get_data(self):
232 """
233 Retrieve the relation data and store it under `self[self.interface]`.
234
235 If there are more than one units related on the desired interface,
236 then each unit will have its data stored under `self[self.interface][unit_id]`
237 and one of the units with complete information will chosen at random
238 to fill the values at `self[self.interface]`.
239
240
241 For example:
242
243 {
244 'foo': 'bar',
245 'unit/0': {
246 'foo': 'bar',
247 },
248 'unit/1': {
249 'foo': 'baz',
250 },
251 }
252 """
253 if not hookenv.relation_ids(self.interface):
254 return
255
256 ns = self.setdefault(self.interface, {})
257 required = set(self.required_keys)
258 for rid in hookenv.relation_ids(self.interface):
259 for unit in hookenv.related_units(rid):
260 reldata = hookenv.relation_get(rid=rid, unit=unit)
261 unit_ns = ns.setdefault(unit, {})
262 unit_ns.update(reldata)
263 if set(reldata.keys()).issuperset(required):
264 ns.update(reldata)
265
266
267class ManagerCallback(object):
268 """
269 Special case of a callback that takes the `ServiceManager` instance
270 in addition to the service name.
271
272 Subclasses should implement `__call__` which should accept two parameters:
273
274 * `manager` The `ServiceManager` instance
275 * `service_name` The name of the service it's being triggered for
276 * `event_name` The name of the event that this callback is handling
277 """
278 def __call__(self, manager, service_name, event_name):
70 raise NotImplementedError()279 raise NotImplementedError()
71280
72281
73class UpstartService(ServiceTypeManager):282class TemplateCallback(ManagerCallback):
74 def __init__(self, service_definition):283 """
75 super(UpstartService, self).__init__(service_definition)284 Callback class that will render a template, for use as a ready action.
76 for tmpl in self.templates:285
77 if 'target' not in tmpl:286 The `target` param, if omitted, will default to `/etc/init/<service name>`.
78 tmpl['target'] = '/etc/init/%s.conf' % self.service_name287 """
79288 def __init__(self, source, target, owner='root', group='root', perms=0444):
80 def reconfigure(self, restart):289 self.source = source
81 complete = templating.render(self.templates, self.templates_dir)290 self.target = target
82291 self.owner = owner
83 if restart and complete:292 self.group = group
84 host.service_restart(self.service_name)293 self.perms = perms
294
295 def __call__(self, manager, service_name, event_name):
296 service = manager.get_service(service_name)
297 context = {}
298 for ctx in service.get('required_data', []):
299 context.update(ctx)
300 templating.render(self.source, self.target, context,
301 self.owner, self.group, self.perms)
302
303
304class PortManagerCallback(ManagerCallback):
305 """
306 Callback class that will open or close ports, for use as either
307 a start or stop action.
308 """
309 def __call__(self, manager, service_name, event_name):
310 service = manager.get_service(service_name)
311 for port in service.get('ports', []):
312 if event_name == 'start':
313 hookenv.open_port(port)
314 elif event_name == 'stop':
315 hookenv.close_port(port)
316
317
318# Convenience aliases
319template = TemplateCallback
320open_ports = PortManagerCallback()
321close_ports = PortManagerCallback()
85322
=== modified file 'hooks/charmhelpers/core/templating.py'
--- hooks/charmhelpers/core/templating.py 2014-05-20 19:43:29 +0000
+++ hooks/charmhelpers/core/templating.py 2014-05-29 17:26:14 +0000
@@ -1,158 +1,51 @@
1import os1import os
2import yaml
32
4from charmhelpers.core import host3from charmhelpers.core import host
5from charmhelpers.core import hookenv4from charmhelpers.core import hookenv
65
76
8class ContextGenerator(object):7def render(source, target, context, owner='root', group='root', perms=0444, templates_dir=None):
9 """8 """
10 Base interface for template context container generators.9 Render a template.
1110
12 A template context is a dictionary that contains data needed to populate11 The `source` path, if not absolute, is relative to the `templates_dir`.
13 the template. The generator instance should produce the context when
14 called (without arguments) by collecting information from juju (config-get,
15 relation-get, etc), the system, or whatever other sources are appropriate.
16
17 A context generator should only return any values if it has enough information
18 to provide all of its values. Any context that is missing data is considered
19 incomplete and will cause that template to not render until it has all of its
20 necessary data.
21
22 The template may receive several contexts, which will be merged together,
23 so care should be taken in the key names.
24 """
25 def __call__(self):
26 raise NotImplementedError
27
28
29class StorableContext(object):
30 """
31 A mixin for persisting a context to disk.
32 """
33 def store_context(self, file_name, config_data):
34 with open(file_name, 'w') as file_stream:
35 yaml.dump(config_data, file_stream)
36
37 def read_context(self, file_name):
38 with open(file_name, 'r') as file_stream:
39 data = yaml.load(file_stream)
40 if not data:
41 raise OSError("%s is empty" % file_name)
42 return data
43
44
45class ConfigContext(ContextGenerator):
46 """
47 A context generator that generates a context containing all of the
48 juju config values.
49 """
50 def __call__(self):
51 return hookenv.config()
52
53
54class RelationContext(ContextGenerator):
55 """
56 Base class for a context generator that gets relation data from juju.
57
58 Subclasses must provide `interface`, which is the interface type of interest,
59 and `required_keys`, which is the set of keys required for the relation to
60 be considered complete. The first relation for the interface that is complete
61 will be used to populate the data for template.
62
63 The generated context will be namespaced under the interface type, to prevent
64 potential naming conflicts.
65 """
66 interface = None
67 required_keys = []
68
69 def __call__(self):
70 if not hookenv.relation_ids(self.interface):
71 return {}
72
73 ctx = {}
74 for rid in hookenv.relation_ids(self.interface):
75 for unit in hookenv.related_units(rid):
76 reldata = hookenv.relation_get(rid=rid, unit=unit)
77 required = set(self.required_keys)
78 if set(reldata.keys()).issuperset(required):
79 ns = ctx.setdefault(self.interface, {})
80 for k, v in reldata.items():
81 ns[k] = v
82 return ctx
83
84 return {}
85
86
87class StaticContext(ContextGenerator):
88 def __init__(self, data):
89 self.data = data
90
91 def __call__(self):
92 return self.data
93
94
95def _collect_contexts(context_providers):
96 """
97 Helper function to collect and merge contexts from a list of providers.
98
99 If any of the contexts are incomplete (i.e., they return an empty dict),
100 the template is considered incomplete and will not render.
101 """
102 ctx = {}
103 for provider in context_providers:
104 c = provider()
105 if not c:
106 return False
107 ctx.update(c)
108 return ctx
109
110
111def render(template_definitions, templates_dir=None):
112 """
113 Render one or more templates, given a list of template definitions.
114
115 The template definitions should be dicts with the keys: `source`, `target`,
116 `file_properties`, and `contexts`.
117
118 The `source` path, if not absolute, is relative to the `templates_dir`
119 given when the rendered was created. If `source` is not provided
120 for a template the `template_dir` will be consulted for
121 ``basename(target).j2``.
12212
123 The `target` path should be absolute.13 The `target` path should be absolute.
12414
125 The `file_properties` should be a dict optionally containing15 The context should be a dict containing the values to be replaced in the
126 `owner`, `group`, or `perms` options, to be passed to `write_file`.16 template.
12717
128 The `contexts` should be a list containing zero or more ContextGenerators.18 The `owner`, `group`, and `perms` options will be passed to `write_file`.
12919
130 The `template_dir` defaults to `$CHARM_DIR/templates`20 If omitted, `templates_dir` defaults to the `templates` folder in the charm.
13121
132 Returns True if all of the templates were "complete" (i.e., the context22 Note: Using this requires python-jinja2; if it is not installed, calling
133 generators were able to collect the information needed to render the23 this will attempt to use charmhelpers.fetch.apt_install to install it.
134 template) and were rendered.
135 """24 """
136 # lazy import jinja2 in case templating is needed in install hook25 try:
137 from jinja2 import FileSystemLoader, Environment, exceptions26 from jinja2 import FileSystemLoader, Environment, exceptions
138 all_complete = True27 except ImportError:
28 try:
29 from charmhelpers.fetch import apt_install
30 except ImportError:
31 hookenv.log('Could not import jinja2, and could not import '
32 'charmhelpers.fetch to install it',
33 level=hookenv.ERROR)
34 raise
35 apt_install('python-jinja2', fatal=True)
36 from jinja2 import FileSystemLoader, Environment, exceptions
37
139 if templates_dir is None:38 if templates_dir is None:
140 templates_dir = os.path.join(hookenv.charm_dir(), 'templates')39 templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
141 loader = Environment(loader=FileSystemLoader(templates_dir))40 loader = Environment(loader=FileSystemLoader(templates_dir))
142 for tmpl in template_definitions:41 try:
143 ctx = _collect_contexts(tmpl.get('contexts', []))42 source = source
144 if ctx is False:43 template = loader.get_template(source)
145 all_complete = False44 except exceptions.TemplateNotFound as e:
146 continue45 hookenv.log('Could not load template %s from %s.' %
147 try:46 (source, templates_dir),
148 source = tmpl.get('source', os.path.basename(tmpl['target'])+'.j2')47 level=hookenv.ERROR)
149 template = loader.get_template(source)48 raise e
150 except exceptions.TemplateNotFound as e:49 content = template.render(context)
151 hookenv.log('Could not load template %s from %s.' %50 host.mkdir(os.path.dirname(target))
152 (tmpl['source'], templates_dir),51 host.write_file(target, content, owner, group, perms)
153 level=hookenv.ERROR)
154 raise e
155 content = template.render(ctx)
156 host.mkdir(os.path.dirname(tmpl['target']))
157 host.write_file(tmpl['target'], content, **tmpl.get('file_properties', {}))
158 return all_complete
15952
=== modified symlink 'hooks/config-changed' (properties changed: -x to +x)
=== target was u'hooks.py'
--- hooks/config-changed 1970-01-01 00:00:00 +0000
+++ hooks/config-changed 2014-05-29 17:26:14 +0000
@@ -0,0 +1,9 @@
1#!/usr/bin/env python
2# vim: et ai ts=4 sw=4:
3from charmhelpers.core import services
4
5import config
6
7
8manager = services.ServiceManager(config.SERVICES)
9manager.manage()
010
=== modified file 'hooks/config.py'
--- hooks/config.py 2014-05-20 19:50:35 +0000
+++ hooks/config.py 2014-05-29 17:26:14 +0000
@@ -1,4 +1,10 @@
1import os1import os
2import subprocess
3
4from charmhelpers.core import host
5from charmhelpers.core import hookenv
6from charmhelpers.core import services
7from charmhelpers.contrib.cloudfoundry import contexts
28
39
4__all__ = ['CF_DIR', 'CC_PACKAGES', 'CC_DIR', 'CC_CONFIG_DIR',10__all__ = ['CF_DIR', 'CC_PACKAGES', 'CC_DIR', 'CC_CONFIG_DIR',
@@ -23,3 +29,36 @@
23NGINX_RUN_DIR = '/var/vcap/sys/run/nginx_ccng'29NGINX_RUN_DIR = '/var/vcap/sys/run/nginx_ccng'
24NGINX_LOG_DIR = '/var/vcap/sys/log/nginx_ccng'30NGINX_LOG_DIR = '/var/vcap/sys/log/nginx_ccng'
25FOG_CONNECTION = '/var/vcap/nfs/store'31FOG_CONNECTION = '/var/vcap/nfs/store'
32
33
34def db_migrate(service_name):
35 hookenv.log("Starting db:migrate...", hookenv.DEBUG)
36 with host.chdir(CC_DIR):
37 subprocess.check_call([
38 'sudo', '-u', 'vcap', '-g', 'vcap',
39 'CLOUD_CONTROLLER_NG_CONFIG={}'.format(CC_CONFIG_FILE),
40 'bundle', 'exec', 'rake', 'db:migrate'])
41 hookenv.log("Finished db:migrate", hookenv.DEBUG)
42
43
44SERVICES = [
45 {
46 'service': 'cf-cloudcontroller',
47 'required_data': [contexts.NatsRelation(),
48 contexts.RouterRelation(),
49 contexts.MysqlRelation()],
50 'data_ready': [
51 services.template(source='cf-cloudcontroller.conf',
52 target='/etc/init/cf-cloudcontroller.conf'),
53 services.template(source='cloud_controller.yml',
54 target=CC_CONFIG_FILE,
55 owner='vcap'),
56 db_migrate,
57 ],
58 },
59 {
60 'service': 'cf-cloudcontroller-job',
61 'data_ready': services.template(source='cf-cloudcontroller-job.conf',
62 target='/etc/init/cf-cloudcontroller-job.conf'),
63 },
64]
2665
=== modified symlink 'hooks/db-relation-changed' (properties changed: -x to +x)
=== target was u'hooks.py'
--- hooks/db-relation-changed 1970-01-01 00:00:00 +0000
+++ hooks/db-relation-changed 2014-05-29 17:26:14 +0000
@@ -0,0 +1,9 @@
1#!/usr/bin/env python
2# vim: et ai ts=4 sw=4:
3from charmhelpers.core import services
4
5import config
6
7
8manager = services.ServiceManager(config.SERVICES)
9manager.manage()
010
=== removed file 'hooks/hooks.py'
--- hooks/hooks.py 2014-05-20 19:50:35 +0000
+++ hooks/hooks.py 1970-01-01 00:00:00 +0000
@@ -1,81 +0,0 @@
1#!/usr/bin/env python
2# vim: et ai ts=4 sw=4:
3import os
4import sys
5import subprocess
6
7from charmhelpers.core import host
8from charmhelpers.core import hookenv
9from charmhelpers.core.hookenv import log
10from charmhelpers.core import services
11from charmhelpers.contrib.cloudfoundry import contexts
12
13import config
14
15hooks = hookenv.Hooks()
16fileproperties = {'owner': 'vcap'}
17
18services.register([
19 {
20 'service': 'cf-cloudcontroller',
21 'templates': [
22 {'source': 'cf-cloudcontroller.conf'},
23 {'source': 'cloud_controller.yml',
24 'target': config.CC_CONFIG_FILE,
25 'file_properties': fileproperties,
26 'contexts': [contexts.NatsContext(),
27 contexts.RouterContext(),
28 contexts.MysqlDSNContext()]}
29 ],
30 },
31 {
32 'service': 'cf-cloudcontroller-job',
33 'templates': [{'source': 'cf-cloudcontroller-job.conf'}],
34 },
35])
36
37
38@hooks.hook('upgrade-charm')
39def upgrade_charm():
40 pass
41
42
43@hooks.hook("config-changed")
44def config_changed():
45 services.reconfigure_services()
46
47
48@hooks.hook()
49def stop():
50 services.stop_services()
51
52
53@hooks.hook('db-relation-changed')
54def db_relation_changed():
55 services.reconfigure_services()
56 hookenv.log("Starting db:migrate...", hookenv.DEBUG)
57 with host.chdir(config.CC_DIR) as dir:
58 subprocess.check_call([
59 'sudo', '-u', 'vcap', '-g', 'vcap',
60 'CLOUD_CONTROLLER_NG_CONFIG={}'.format(config.CC_CONFIG_FILE),
61 'bundle', 'exec', 'rake', 'db:migrate'])
62 hookenv.log("Finished db:migrate in %s." % (dir))
63
64
65@hooks.hook('nats-relation-changed')
66def nats_relation_changed():
67 services.reconfigure_services()
68
69
70@hooks.hook('router-relation-changed')
71def router_relation_changed():
72 services.reconfigure_services()
73
74
75if __name__ == '__main__':
76 hook_name = os.path.basename(sys.argv[0])
77 log("Running {} hook".format(hook_name))
78 if hookenv.relation_id():
79 log("Relation {} with {}".format(
80 hookenv.relation_id(), hookenv.remote_unit()))
81 hooks.execute(sys.argv)
820
=== modified symlink 'hooks/nats-relation-changed' (properties changed: -x to +x)
=== target was u'hooks.py'
--- hooks/nats-relation-changed 1970-01-01 00:00:00 +0000
+++ hooks/nats-relation-changed 2014-05-29 17:26:14 +0000
@@ -0,0 +1,9 @@
1#!/usr/bin/env python
2# vim: et ai ts=4 sw=4:
3from charmhelpers.core import services
4
5import config
6
7
8manager = services.ServiceManager(config.SERVICES)
9manager.manage()
010
=== modified symlink 'hooks/router-relation-changed' (properties changed: -x to +x)
=== target was u'hooks.py'
--- hooks/router-relation-changed 1970-01-01 00:00:00 +0000
+++ hooks/router-relation-changed 2014-05-29 17:26:14 +0000
@@ -0,0 +1,9 @@
1#!/usr/bin/env python
2# vim: et ai ts=4 sw=4:
3from charmhelpers.core import services
4
5import config
6
7
8manager = services.ServiceManager(config.SERVICES)
9manager.manage()
010
=== modified symlink 'hooks/stop' (properties changed: -x to +x)
=== target was u'hooks.py'
--- hooks/stop 1970-01-01 00:00:00 +0000
+++ hooks/stop 2014-05-29 17:26:14 +0000
@@ -0,0 +1,9 @@
1#!/usr/bin/env python
2# vim: et ai ts=4 sw=4:
3from charmhelpers.core import services
4
5import config
6
7
8manager = services.ServiceManager(config.SERVICES)
9manager.manage()
010
=== modified symlink 'hooks/upgrade-charm' (properties changed: -x to +x)
=== target was u'hooks.py'
--- hooks/upgrade-charm 1970-01-01 00:00:00 +0000
+++ hooks/upgrade-charm 2014-05-29 17:26:14 +0000
@@ -0,0 +1,9 @@
1#!/usr/bin/env python
2# vim: et ai ts=4 sw=4:
3from charmhelpers.core import services
4
5import config
6
7
8manager = services.ServiceManager(config.SERVICES)
9manager.manage()

Subscribers

People subscribed via source and target branches