Merge lp:~johnsca/charms/trusty/cf-loggregator/services-callback-fu into lp:~cf-charmers/charms/trusty/cf-loggregator/trunk
- Trusty Tahr (14.04)
- services-callback-fu
- Merge into trunk
Proposed by
Cory Johns
Status: | Merged |
---|---|
Merged at revision: | 32 |
Proposed branch: | lp:~johnsca/charms/trusty/cf-loggregator/services-callback-fu |
Merge into: | lp:~cf-charmers/charms/trusty/cf-loggregator/trunk |
Diff against target: |
896 lines (+448/-332) 11 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 (+5/-0) hooks/config.py (+20/-0) hooks/hooks.py (+0/-76) hooks/loggregator-relation-changed (+8/-0) hooks/nats-relation-changed (+5/-0) hooks/stop (+5/-0) hooks/upgrade-charm (+5/-0) |
To merge this branch: | bzr merge lp:~johnsca/charms/trusty/cf-loggregator/services-callback-fu |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Cloud Foundry Charmers | Pending | ||
Review via email: mp+221444@code.launchpad.net |
Commit message
Description of the change
Refactor to callback services API
To post a comment you must log in.
Revision history for this message
Cory Johns (johnsca) wrote : | # |
Revision history for this message
Benjamin Saller (bcsaller) wrote : | # |
LGTM, though for some of these the rietveld isn't showing the hooks in a
reasonable way so I have to look at the branch code.
Revision history for this message
Cory Johns (johnsca) wrote : | # |
*** Submitted:
Refactor to callback services API
R=benjamin.saller
CC=
https:/
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'hooks/charmhelpers/contrib/cloudfoundry/contexts.py' |
2 | --- hooks/charmhelpers/contrib/cloudfoundry/contexts.py 2014-05-20 19:52:38 +0000 |
3 | +++ hooks/charmhelpers/contrib/cloudfoundry/contexts.py 2014-05-29 18:29:18 +0000 |
4 | @@ -1,55 +1,64 @@ |
5 | import os |
6 | - |
7 | -from charmhelpers.core.templating import ( |
8 | - ContextGenerator, |
9 | - RelationContext, |
10 | - StorableContext, |
11 | -) |
12 | - |
13 | - |
14 | -# Stores `config_data` hash into yaml file with `file_name` as a name |
15 | -# if `file_name` already exists, then it loads data from `file_name`. |
16 | -class StoredContext(ContextGenerator, StorableContext): |
17 | +import yaml |
18 | + |
19 | +from charmhelpers.core.services import RelationContext |
20 | + |
21 | + |
22 | +class StoredContext(dict): |
23 | + """ |
24 | + A data context that always returns the data that it was first created with. |
25 | + """ |
26 | def __init__(self, file_name, config_data): |
27 | + """ |
28 | + If the file exists, populate `self` with the data from the file. |
29 | + Otherwise, populate with the given data and persist it to the file. |
30 | + """ |
31 | if os.path.exists(file_name): |
32 | - self.data = self.read_context(file_name) |
33 | + self.update(self.read_context(file_name)) |
34 | else: |
35 | self.store_context(file_name, config_data) |
36 | - self.data = config_data |
37 | - |
38 | - def __call__(self): |
39 | - return self.data |
40 | - |
41 | - |
42 | -class NatsContext(RelationContext): |
43 | + self.update(config_data) |
44 | + |
45 | + def store_context(self, file_name, config_data): |
46 | + with open(file_name, 'w') as file_stream: |
47 | + yaml.dump(config_data, file_stream) |
48 | + |
49 | + def read_context(self, file_name): |
50 | + with open(file_name, 'r') as file_stream: |
51 | + data = yaml.load(file_stream) |
52 | + if not data: |
53 | + raise OSError("%s is empty" % file_name) |
54 | + return data |
55 | + |
56 | + |
57 | +class NatsRelation(RelationContext): |
58 | interface = 'nats' |
59 | required_keys = ['nats_port', 'nats_address', 'nats_user', 'nats_password'] |
60 | |
61 | |
62 | -class MysqlDSNContext(RelationContext): |
63 | +class MysqlRelation(RelationContext): |
64 | interface = 'db' |
65 | required_keys = ['user', 'password', 'host', 'database'] |
66 | dsn_template = "mysql2://{user}:{password}@{host}:{port}/{database}" |
67 | |
68 | - def __call__(self): |
69 | - ctx = RelationContext.__call__(self) |
70 | - if ctx: |
71 | - if 'port' not in ctx: |
72 | - ctx['db']['port'] = '3306' |
73 | - ctx['db']['dsn'] = self.dsn_template.format(**ctx['db']) |
74 | - return ctx |
75 | - |
76 | - |
77 | -class RouterContext(RelationContext): |
78 | + def get_data(self): |
79 | + RelationContext.get_data(self) |
80 | + if self.is_ready(): |
81 | + if 'port' not in self['db']: |
82 | + self['db']['port'] = '3306' |
83 | + self['db']['dsn'] = self.dsn_template.format(**self['db']) |
84 | + |
85 | + |
86 | +class RouterRelation(RelationContext): |
87 | interface = 'router' |
88 | required_keys = ['domain'] |
89 | |
90 | |
91 | -class LogRouterContext(RelationContext): |
92 | +class LogRouterRelation(RelationContext): |
93 | interface = 'logrouter' |
94 | required_keys = ['shared-secret', 'logrouter-address'] |
95 | |
96 | |
97 | -class LoggregatorContext(RelationContext): |
98 | +class LoggregatorRelation(RelationContext): |
99 | interface = 'loggregator' |
100 | required_keys = ['shared_secret', 'loggregator_address'] |
101 | |
102 | === modified file 'hooks/charmhelpers/core/host.py' |
103 | --- hooks/charmhelpers/core/host.py 2014-05-20 19:52:38 +0000 |
104 | +++ hooks/charmhelpers/core/host.py 2014-05-29 18:29:18 +0000 |
105 | @@ -63,6 +63,11 @@ |
106 | return False |
107 | |
108 | |
109 | +def service_available(service_name): |
110 | + """Determine whether a system service is available""" |
111 | + return service('status', service_name) |
112 | + |
113 | + |
114 | def adduser(username, password=None, shell='/bin/bash', system_user=False): |
115 | """Add a user to the system""" |
116 | try: |
117 | |
118 | === modified file 'hooks/charmhelpers/core/services.py' |
119 | --- hooks/charmhelpers/core/services.py 2014-05-16 22:36:57 +0000 |
120 | +++ hooks/charmhelpers/core/services.py 2014-05-29 18:29:18 +0000 |
121 | @@ -1,84 +1,321 @@ |
122 | +import os |
123 | +import sys |
124 | +from collections import Iterable |
125 | from charmhelpers.core import templating |
126 | from charmhelpers.core import host |
127 | - |
128 | - |
129 | -SERVICES = {} |
130 | - |
131 | - |
132 | -def register(services, templates_dir=None): |
133 | - """ |
134 | - Register a list of service configs. |
135 | - |
136 | - Service Configs are dicts in the following formats: |
137 | - |
138 | - { |
139 | - "service": <service name>, |
140 | - "templates": [ { |
141 | - 'target': <render target of template>, |
142 | - 'source': <optional name of template in passed in templates_dir> |
143 | - 'file_properties': <optional dict taking owner and octal mode> |
144 | - 'contexts': [ context generators, see contexts.py ] |
145 | - } |
146 | - ] } |
147 | - |
148 | - Either `source` or `target` must be provided. |
149 | - |
150 | - If 'source' is not provided for a template the templates_dir will |
151 | - be consulted for ``basename(target).j2``. |
152 | - |
153 | - If `target` is not provided, it will be assumed to be |
154 | - ``/etc/init/<service name>.conf``. |
155 | - """ |
156 | - for service in services: |
157 | - service.setdefault('templates_dir', templates_dir) |
158 | - SERVICES[service['service']] = service |
159 | - |
160 | - |
161 | -def reconfigure_services(restart=True): |
162 | - """ |
163 | - Update all files for all services and optionally restart them, if ready. |
164 | - """ |
165 | - for service_name in SERVICES.keys(): |
166 | - reconfigure_service(service_name, restart=restart) |
167 | - |
168 | - |
169 | -def reconfigure_service(service_name, restart=True): |
170 | - """ |
171 | - Update all files for a single service and optionally restart it, if ready. |
172 | - """ |
173 | - service = SERVICES.get(service_name) |
174 | - if not service or service['service'] != service_name: |
175 | - raise KeyError('Service not registered: %s' % service_name) |
176 | - |
177 | - manager_type = service.get('type', UpstartService) |
178 | - manager_type(service).reconfigure(restart) |
179 | - |
180 | - |
181 | -def stop_services(): |
182 | - for service_name in SERVICES.keys(): |
183 | - if host.service_running(service_name): |
184 | - host.service_stop(service_name) |
185 | - |
186 | - |
187 | -class ServiceTypeManager(object): |
188 | - def __init__(self, service_definition): |
189 | - self.service_name = service_definition['service'] |
190 | - self.templates = service_definition['templates'] |
191 | - self.templates_dir = service_definition['templates_dir'] |
192 | - |
193 | - def reconfigure(self, restart=True): |
194 | +from charmhelpers.core import hookenv |
195 | + |
196 | + |
197 | +class ServiceManager(object): |
198 | + def __init__(self, services=None): |
199 | + """ |
200 | + Register a list of services, given their definitions. |
201 | + |
202 | + Service definitions are dicts in the following formats (all keys except |
203 | + 'service' are optional): |
204 | + |
205 | + { |
206 | + "service": <service name>, |
207 | + "required_data": <list of required data contexts>, |
208 | + "data_ready": <one or more callbacks>, |
209 | + "data_lost": <one or more callbacks>, |
210 | + "start": <one or more callbacks>, |
211 | + "stop": <one or more callbacks>, |
212 | + "ports": <list of ports to manage>, |
213 | + } |
214 | + |
215 | + The 'required_data' list should contain dicts of required data (or |
216 | + dependency managers that act like dicts and know how to collect the data). |
217 | + Only when all items in the 'required_data' list are populated are the list |
218 | + of 'data_ready' and 'start' callbacks executed. See `is_ready()` for more |
219 | + information. |
220 | + |
221 | + The 'data_ready' value should be either a single callback, or a list of |
222 | + callbacks, to be called when all items in 'required_data' pass `is_ready()`. |
223 | + Each callback will be called with the service name as the only parameter. |
224 | + After these all of the 'data_ready' callbacks are called, the 'start' |
225 | + callbacks are fired. |
226 | + |
227 | + The 'data_lost' value should be either a single callback, or a list of |
228 | + callbacks, to be called when a 'required_data' item no longer passes |
229 | + `is_ready()`. Each callback will be called with the service name as the |
230 | + only parameter. After these all of the 'data_ready' callbacks are called, |
231 | + the 'stop' callbacks are fired. |
232 | + |
233 | + The 'start' value should be either a single callback, or a list of |
234 | + callbacks, to be called when starting the service, after the 'data_ready' |
235 | + callbacks are complete. Each callback will be called with the service |
236 | + name as the only parameter. This defaults to |
237 | + `[host.service_start, services.open_ports]`. |
238 | + |
239 | + The 'stop' value should be either a single callback, or a list of |
240 | + callbacks, to be called when stopping the service. If the service is |
241 | + being stopped because it no longer has all of its 'required_data', this |
242 | + will be called after all of the 'data_lost' callbacks are complete. |
243 | + Each callback will be called with the service name as the only parameter. |
244 | + This defaults to `[services.close_ports, host.service_stop]`. |
245 | + |
246 | + The 'ports' value should be a list of ports to manage. The default |
247 | + 'start' handler will open the ports after the service is started, |
248 | + and the default 'stop' handler will close the ports prior to stopping |
249 | + the service. |
250 | + |
251 | + |
252 | + Examples: |
253 | + |
254 | + The following registers an Upstart service called bingod that depends on |
255 | + a mongodb relation and which runs a custom `db_migrate` function prior to |
256 | + restarting the service, and a Runit serivce called spadesd. |
257 | + |
258 | + >>> manager = services.ServiceManager([ |
259 | + ... { |
260 | + ... 'service': 'bingod', |
261 | + ... 'ports': [80, 443], |
262 | + ... 'required_data': [MongoRelation(), config()], |
263 | + ... 'data_ready': [ |
264 | + ... services.template(source='bingod.conf'), |
265 | + ... services.template(source='bingod.ini', |
266 | + ... target='/etc/bingod.ini', |
267 | + ... owner='bingo', perms=0400), |
268 | + ... ], |
269 | + ... }, |
270 | + ... { |
271 | + ... 'service': 'spadesd', |
272 | + ... 'data_ready': services.template(source='spadesd_run.j2', |
273 | + ... target='/etc/sv/spadesd/run', |
274 | + ... perms=0555), |
275 | + ... 'start': runit_start, |
276 | + ... 'stop': runit_stop, |
277 | + ... }, |
278 | + ... ]) |
279 | + ... manager.manage() |
280 | + """ |
281 | + self.services = {} |
282 | + for service in services or []: |
283 | + service_name = service['service'] |
284 | + self.services[service_name] = service |
285 | + |
286 | + def manage(self): |
287 | + """ |
288 | + Handle the current hook by doing The Right Thing with the registered services. |
289 | + """ |
290 | + hook_name = os.path.basename(sys.argv[0]) |
291 | + if hook_name == 'stop': |
292 | + self.stop_services() |
293 | + else: |
294 | + self.reconfigure_services() |
295 | + |
296 | + def reconfigure_services(self, *service_names): |
297 | + """ |
298 | + Update all files for one or more registered services, and, |
299 | + if ready, optionally restart them. |
300 | + |
301 | + If no service names are given, reconfigures all registered services. |
302 | + """ |
303 | + for service_name in service_names or self.services.keys(): |
304 | + if self.is_ready(service_name): |
305 | + self.fire_event('data_ready', service_name) |
306 | + self.fire_event('start', service_name, default=[ |
307 | + host.service_restart, |
308 | + open_ports]) |
309 | + self.save_ready(service_name) |
310 | + else: |
311 | + if self.was_ready(service_name): |
312 | + self.fire_event('data_lost', service_name) |
313 | + self.fire_event('stop', service_name, default=[ |
314 | + close_ports, |
315 | + host.service_stop]) |
316 | + self.save_lost(service_name) |
317 | + |
318 | + def stop_services(self, *service_names): |
319 | + """ |
320 | + Stop one or more registered services, by name. |
321 | + |
322 | + If no service names are given, stops all registered services. |
323 | + """ |
324 | + for service_name in service_names or self.services.keys(): |
325 | + self.fire_event('stop', service_name, default=[ |
326 | + close_ports, |
327 | + host.service_stop]) |
328 | + |
329 | + def get_service(self, service_name): |
330 | + """ |
331 | + Given the name of a registered service, return its service definition. |
332 | + """ |
333 | + service = self.services.get(service_name) |
334 | + if not service: |
335 | + raise KeyError('Service not registered: %s' % service_name) |
336 | + return service |
337 | + |
338 | + def fire_event(self, event_name, service_name, default=None): |
339 | + """ |
340 | + Fire a data_ready, data_lost, start, or stop event on a given service. |
341 | + """ |
342 | + service = self.get_service(service_name) |
343 | + callbacks = service.get(event_name, default) |
344 | + if not callbacks: |
345 | + return |
346 | + if not isinstance(callbacks, Iterable): |
347 | + callbacks = [callbacks] |
348 | + for callback in callbacks: |
349 | + if isinstance(callback, ManagerCallback): |
350 | + callback(self, service_name, event_name) |
351 | + else: |
352 | + callback(service_name) |
353 | + |
354 | + def is_ready(self, service_name): |
355 | + """ |
356 | + Determine if a registered service is ready, by checking its 'required_data'. |
357 | + |
358 | + A 'required_data' item can be any mapping type, and is considered ready |
359 | + if `bool(item)` evaluates as True. |
360 | + """ |
361 | + service = self.get_service(service_name) |
362 | + reqs = service.get('required_data', []) |
363 | + return all(bool(req) for req in reqs) |
364 | + |
365 | + def save_ready(self, service_name): |
366 | + """ |
367 | + Save an indicator that the given service is now data_ready. |
368 | + """ |
369 | + ready_file = '{}/.ready.{}'.format(hookenv.charm_dir(), service_name) |
370 | + with open(ready_file, 'a'): |
371 | + pass |
372 | + |
373 | + def save_lost(self, service_name): |
374 | + """ |
375 | + Save an indicator that the given service is no longer data_ready. |
376 | + """ |
377 | + ready_file = '{}/.ready.{}'.format(hookenv.charm_dir(), service_name) |
378 | + if os.path.exists(ready_file): |
379 | + os.remove(ready_file) |
380 | + |
381 | + def was_ready(self, service_name): |
382 | + """ |
383 | + Determine if the given service was previously data_ready. |
384 | + """ |
385 | + ready_file = '{}/.ready.{}'.format(hookenv.charm_dir(), service_name) |
386 | + return os.path.exists(ready_file) |
387 | + |
388 | + |
389 | +class RelationContext(dict): |
390 | + """ |
391 | + Base class for a context generator that gets relation data from juju. |
392 | + |
393 | + Subclasses must provide `interface`, which is the interface type of interest, |
394 | + and `required_keys`, which is the set of keys required for the relation to |
395 | + be considered complete. The first relation for the interface that is complete |
396 | + will be used to populate the data for template. |
397 | + |
398 | + The generated context will be namespaced under the interface type, to prevent |
399 | + potential naming conflicts. |
400 | + """ |
401 | + interface = None |
402 | + required_keys = [] |
403 | + |
404 | + def __bool__(self): |
405 | + """ |
406 | + Updates the data and returns True if all of the required_keys are available. |
407 | + """ |
408 | + self.get_data() |
409 | + return self.is_ready() |
410 | + |
411 | + __nonzero__ = __bool__ |
412 | + |
413 | + def is_ready(self): |
414 | + """ |
415 | + Returns True if all of the required_keys are available. |
416 | + """ |
417 | + return set(self.get(self.interface, {}).keys()).issuperset(set(self.required_keys)) |
418 | + |
419 | + def get_data(self): |
420 | + """ |
421 | + Retrieve the relation data and store it under `self[self.interface]`. |
422 | + |
423 | + If there are more than one units related on the desired interface, |
424 | + then each unit will have its data stored under `self[self.interface][unit_id]` |
425 | + and one of the units with complete information will chosen at random |
426 | + to fill the values at `self[self.interface]`. |
427 | + |
428 | + |
429 | + For example: |
430 | + |
431 | + { |
432 | + 'foo': 'bar', |
433 | + 'unit/0': { |
434 | + 'foo': 'bar', |
435 | + }, |
436 | + 'unit/1': { |
437 | + 'foo': 'baz', |
438 | + }, |
439 | + } |
440 | + """ |
441 | + if not hookenv.relation_ids(self.interface): |
442 | + return |
443 | + |
444 | + ns = self.setdefault(self.interface, {}) |
445 | + required = set(self.required_keys) |
446 | + for rid in hookenv.relation_ids(self.interface): |
447 | + for unit in hookenv.related_units(rid): |
448 | + reldata = hookenv.relation_get(rid=rid, unit=unit) |
449 | + unit_ns = ns.setdefault(unit, {}) |
450 | + unit_ns.update(reldata) |
451 | + if set(reldata.keys()).issuperset(required): |
452 | + ns.update(reldata) |
453 | + |
454 | + |
455 | +class ManagerCallback(object): |
456 | + """ |
457 | + Special case of a callback that takes the `ServiceManager` instance |
458 | + in addition to the service name. |
459 | + |
460 | + Subclasses should implement `__call__` which should accept two parameters: |
461 | + |
462 | + * `manager` The `ServiceManager` instance |
463 | + * `service_name` The name of the service it's being triggered for |
464 | + * `event_name` The name of the event that this callback is handling |
465 | + """ |
466 | + def __call__(self, manager, service_name, event_name): |
467 | raise NotImplementedError() |
468 | |
469 | |
470 | -class UpstartService(ServiceTypeManager): |
471 | - def __init__(self, service_definition): |
472 | - super(UpstartService, self).__init__(service_definition) |
473 | - for tmpl in self.templates: |
474 | - if 'target' not in tmpl: |
475 | - tmpl['target'] = '/etc/init/%s.conf' % self.service_name |
476 | - |
477 | - def reconfigure(self, restart): |
478 | - complete = templating.render(self.templates, self.templates_dir) |
479 | - |
480 | - if restart and complete: |
481 | - host.service_restart(self.service_name) |
482 | +class TemplateCallback(ManagerCallback): |
483 | + """ |
484 | + Callback class that will render a template, for use as a ready action. |
485 | + |
486 | + The `target` param, if omitted, will default to `/etc/init/<service name>`. |
487 | + """ |
488 | + def __init__(self, source, target, owner='root', group='root', perms=0444): |
489 | + self.source = source |
490 | + self.target = target |
491 | + self.owner = owner |
492 | + self.group = group |
493 | + self.perms = perms |
494 | + |
495 | + def __call__(self, manager, service_name, event_name): |
496 | + service = manager.get_service(service_name) |
497 | + context = {} |
498 | + for ctx in service.get('required_data', []): |
499 | + context.update(ctx) |
500 | + templating.render(self.source, self.target, context, |
501 | + self.owner, self.group, self.perms) |
502 | + |
503 | + |
504 | +class PortManagerCallback(ManagerCallback): |
505 | + """ |
506 | + Callback class that will open or close ports, for use as either |
507 | + a start or stop action. |
508 | + """ |
509 | + def __call__(self, manager, service_name, event_name): |
510 | + service = manager.get_service(service_name) |
511 | + for port in service.get('ports', []): |
512 | + if event_name == 'start': |
513 | + hookenv.open_port(port) |
514 | + elif event_name == 'stop': |
515 | + hookenv.close_port(port) |
516 | + |
517 | + |
518 | +# Convenience aliases |
519 | +template = TemplateCallback |
520 | +open_ports = PortManagerCallback() |
521 | +close_ports = PortManagerCallback() |
522 | |
523 | === modified file 'hooks/charmhelpers/core/templating.py' |
524 | --- hooks/charmhelpers/core/templating.py 2014-05-20 19:52:38 +0000 |
525 | +++ hooks/charmhelpers/core/templating.py 2014-05-29 18:29:18 +0000 |
526 | @@ -1,158 +1,51 @@ |
527 | import os |
528 | -import yaml |
529 | |
530 | from charmhelpers.core import host |
531 | from charmhelpers.core import hookenv |
532 | |
533 | |
534 | -class ContextGenerator(object): |
535 | - """ |
536 | - Base interface for template context container generators. |
537 | - |
538 | - A template context is a dictionary that contains data needed to populate |
539 | - the template. The generator instance should produce the context when |
540 | - called (without arguments) by collecting information from juju (config-get, |
541 | - relation-get, etc), the system, or whatever other sources are appropriate. |
542 | - |
543 | - A context generator should only return any values if it has enough information |
544 | - to provide all of its values. Any context that is missing data is considered |
545 | - incomplete and will cause that template to not render until it has all of its |
546 | - necessary data. |
547 | - |
548 | - The template may receive several contexts, which will be merged together, |
549 | - so care should be taken in the key names. |
550 | - """ |
551 | - def __call__(self): |
552 | - raise NotImplementedError |
553 | - |
554 | - |
555 | -class StorableContext(object): |
556 | - """ |
557 | - A mixin for persisting a context to disk. |
558 | - """ |
559 | - def store_context(self, file_name, config_data): |
560 | - with open(file_name, 'w') as file_stream: |
561 | - yaml.dump(config_data, file_stream) |
562 | - |
563 | - def read_context(self, file_name): |
564 | - with open(file_name, 'r') as file_stream: |
565 | - data = yaml.load(file_stream) |
566 | - if not data: |
567 | - raise OSError("%s is empty" % file_name) |
568 | - return data |
569 | - |
570 | - |
571 | -class ConfigContext(ContextGenerator): |
572 | - """ |
573 | - A context generator that generates a context containing all of the |
574 | - juju config values. |
575 | - """ |
576 | - def __call__(self): |
577 | - return hookenv.config() |
578 | - |
579 | - |
580 | -class RelationContext(ContextGenerator): |
581 | - """ |
582 | - Base class for a context generator that gets relation data from juju. |
583 | - |
584 | - Subclasses must provide `interface`, which is the interface type of interest, |
585 | - and `required_keys`, which is the set of keys required for the relation to |
586 | - be considered complete. The first relation for the interface that is complete |
587 | - will be used to populate the data for template. |
588 | - |
589 | - The generated context will be namespaced under the interface type, to prevent |
590 | - potential naming conflicts. |
591 | - """ |
592 | - interface = None |
593 | - required_keys = [] |
594 | - |
595 | - def __call__(self): |
596 | - if not hookenv.relation_ids(self.interface): |
597 | - return {} |
598 | - |
599 | - ctx = {} |
600 | - for rid in hookenv.relation_ids(self.interface): |
601 | - for unit in hookenv.related_units(rid): |
602 | - reldata = hookenv.relation_get(rid=rid, unit=unit) |
603 | - required = set(self.required_keys) |
604 | - if set(reldata.keys()).issuperset(required): |
605 | - ns = ctx.setdefault(self.interface, {}) |
606 | - for k, v in reldata.items(): |
607 | - ns[k] = v |
608 | - return ctx |
609 | - |
610 | - return {} |
611 | - |
612 | - |
613 | -class StaticContext(ContextGenerator): |
614 | - def __init__(self, data): |
615 | - self.data = data |
616 | - |
617 | - def __call__(self): |
618 | - return self.data |
619 | - |
620 | - |
621 | -def _collect_contexts(context_providers): |
622 | - """ |
623 | - Helper function to collect and merge contexts from a list of providers. |
624 | - |
625 | - If any of the contexts are incomplete (i.e., they return an empty dict), |
626 | - the template is considered incomplete and will not render. |
627 | - """ |
628 | - ctx = {} |
629 | - for provider in context_providers: |
630 | - c = provider() |
631 | - if not c: |
632 | - return False |
633 | - ctx.update(c) |
634 | - return ctx |
635 | - |
636 | - |
637 | -def render(template_definitions, templates_dir=None): |
638 | - """ |
639 | - Render one or more templates, given a list of template definitions. |
640 | - |
641 | - The template definitions should be dicts with the keys: `source`, `target`, |
642 | - `file_properties`, and `contexts`. |
643 | - |
644 | - The `source` path, if not absolute, is relative to the `templates_dir` |
645 | - given when the rendered was created. If `source` is not provided |
646 | - for a template the `template_dir` will be consulted for |
647 | - ``basename(target).j2``. |
648 | +def render(source, target, context, owner='root', group='root', perms=0444, templates_dir=None): |
649 | + """ |
650 | + Render a template. |
651 | + |
652 | + The `source` path, if not absolute, is relative to the `templates_dir`. |
653 | |
654 | The `target` path should be absolute. |
655 | |
656 | - The `file_properties` should be a dict optionally containing |
657 | - `owner`, `group`, or `perms` options, to be passed to `write_file`. |
658 | - |
659 | - The `contexts` should be a list containing zero or more ContextGenerators. |
660 | - |
661 | - The `template_dir` defaults to `$CHARM_DIR/templates` |
662 | - |
663 | - Returns True if all of the templates were "complete" (i.e., the context |
664 | - generators were able to collect the information needed to render the |
665 | - template) and were rendered. |
666 | + The context should be a dict containing the values to be replaced in the |
667 | + template. |
668 | + |
669 | + The `owner`, `group`, and `perms` options will be passed to `write_file`. |
670 | + |
671 | + If omitted, `templates_dir` defaults to the `templates` folder in the charm. |
672 | + |
673 | + Note: Using this requires python-jinja2; if it is not installed, calling |
674 | + this will attempt to use charmhelpers.fetch.apt_install to install it. |
675 | """ |
676 | - # lazy import jinja2 in case templating is needed in install hook |
677 | - from jinja2 import FileSystemLoader, Environment, exceptions |
678 | - all_complete = True |
679 | + try: |
680 | + from jinja2 import FileSystemLoader, Environment, exceptions |
681 | + except ImportError: |
682 | + try: |
683 | + from charmhelpers.fetch import apt_install |
684 | + except ImportError: |
685 | + hookenv.log('Could not import jinja2, and could not import ' |
686 | + 'charmhelpers.fetch to install it', |
687 | + level=hookenv.ERROR) |
688 | + raise |
689 | + apt_install('python-jinja2', fatal=True) |
690 | + from jinja2 import FileSystemLoader, Environment, exceptions |
691 | + |
692 | if templates_dir is None: |
693 | templates_dir = os.path.join(hookenv.charm_dir(), 'templates') |
694 | loader = Environment(loader=FileSystemLoader(templates_dir)) |
695 | - for tmpl in template_definitions: |
696 | - ctx = _collect_contexts(tmpl.get('contexts', [])) |
697 | - if ctx is False: |
698 | - all_complete = False |
699 | - continue |
700 | - try: |
701 | - source = tmpl.get('source', os.path.basename(tmpl['target'])+'.j2') |
702 | - template = loader.get_template(source) |
703 | - except exceptions.TemplateNotFound as e: |
704 | - hookenv.log('Could not load template %s from %s.' % |
705 | - (tmpl['source'], templates_dir), |
706 | - level=hookenv.ERROR) |
707 | - raise e |
708 | - content = template.render(ctx) |
709 | - host.mkdir(os.path.dirname(tmpl['target'])) |
710 | - host.write_file(tmpl['target'], content, **tmpl.get('file_properties', {})) |
711 | - return all_complete |
712 | + try: |
713 | + source = source |
714 | + template = loader.get_template(source) |
715 | + except exceptions.TemplateNotFound as e: |
716 | + hookenv.log('Could not load template %s from %s.' % |
717 | + (source, templates_dir), |
718 | + level=hookenv.ERROR) |
719 | + raise e |
720 | + content = template.render(context) |
721 | + host.mkdir(os.path.dirname(target)) |
722 | + host.write_file(target, content, owner, group, perms) |
723 | |
724 | === modified symlink 'hooks/config-changed' (properties changed: -x to +x) |
725 | === target was u'hooks.py' |
726 | --- hooks/config-changed 1970-01-01 00:00:00 +0000 |
727 | +++ hooks/config-changed 2014-05-29 18:29:18 +0000 |
728 | @@ -0,0 +1,5 @@ |
729 | +#!/usr/bin/env python |
730 | +from charmhelpers.core import services |
731 | +import config |
732 | +manager = services.ServiceManager(config.SERVICES) |
733 | +manager.manage() |
734 | |
735 | === modified file 'hooks/config.py' |
736 | --- hooks/config.py 2014-05-08 11:47:19 +0000 |
737 | +++ hooks/config.py 2014-05-29 18:29:18 +0000 |
738 | @@ -1,4 +1,7 @@ |
739 | import os |
740 | +from charmhelpers.core import services |
741 | +from charmhelpers.core import hookenv |
742 | +from charmhelpers.contrib.cloudfoundry import contexts |
743 | |
744 | __all__ = ['CF_DIR', 'LOGGREGATOR_JOB_NAME', 'LOGGREGATOR_CONFIG_PATH', |
745 | 'LOGGREGATOR_DIR', 'LOGGREGATOR_CONFIG_DIR', 'LOGGREGATOR_PACKAGES'] |
746 | @@ -11,3 +14,20 @@ |
747 | LOGGREGATOR_CONFIG_DIR = os.path.join(LOGGREGATOR_DIR, 'config') |
748 | LOGGREGATOR_CONFIG_PATH = os.path.join(LOGGREGATOR_CONFIG_DIR, |
749 | 'loggregator.json') |
750 | + |
751 | +SERVICES = [ |
752 | + { |
753 | + 'service': LOGGREGATOR_JOB_NAME, |
754 | + 'required_data': [ |
755 | + {'service_name': os.environ['JUJU_UNIT_NAME'].split('/')[0]}, |
756 | + hookenv.config(), |
757 | + contexts.NatsRelation(), |
758 | + ], |
759 | + 'data_ready': [ |
760 | + services.template(source='loggregator.conf', |
761 | + target='/etc/init/loggregator.conf'), |
762 | + services.template(source='loggregator.json', |
763 | + target=LOGGREGATOR_CONFIG_PATH, |
764 | + owner='vcap'), |
765 | + ], |
766 | + }] |
767 | |
768 | === removed file 'hooks/hooks.py' |
769 | --- hooks/hooks.py 2014-05-20 21:53:49 +0000 |
770 | +++ hooks/hooks.py 1970-01-01 00:00:00 +0000 |
771 | @@ -1,76 +0,0 @@ |
772 | -#!/usr/bin/env python |
773 | - |
774 | -import os |
775 | - |
776 | -import sys |
777 | -from charmhelpers.core.hookenv import log |
778 | -from charmhelpers.contrib.cloudfoundry import contexts |
779 | -from charmhelpers.core import services |
780 | -from charmhelpers.core import templating |
781 | -from charmhelpers.core import hookenv |
782 | -import config |
783 | - |
784 | - |
785 | -LOGSVC = "loggregator" |
786 | - |
787 | -hooks = hookenv.Hooks() |
788 | - |
789 | -service_name, _ = os.environ['JUJU_UNIT_NAME'].split('/') |
790 | -fileproperties = {'owner': 'vcap'} |
791 | -services.register([ |
792 | - { |
793 | - 'service': config.LOGGREGATOR_JOB_NAME, |
794 | - 'templates': [ |
795 | - {'source': 'loggregator.conf'}, |
796 | - {'source': 'loggregator.json', |
797 | - 'target': config.LOGGREGATOR_CONFIG_PATH, |
798 | - 'file_properties': fileproperties, |
799 | - 'contexts': [ |
800 | - templating.StaticContext({'service_name': service_name}), |
801 | - templating.ConfigContext(), |
802 | - contexts.NatsContext() |
803 | - ]}, |
804 | - ] |
805 | - }]) |
806 | - |
807 | - |
808 | -@hooks.hook() |
809 | -def upgrade_charm(): |
810 | - pass |
811 | - |
812 | - |
813 | -@hooks.hook() |
814 | -def start(): |
815 | - pass |
816 | - |
817 | - |
818 | -@hooks.hook() |
819 | -def stop(): |
820 | - services.stop_services() |
821 | - |
822 | - |
823 | -@hooks.hook('config-changed') |
824 | -def config_changed(): |
825 | - pass |
826 | - |
827 | - |
828 | -@hooks.hook('loggregator-relation-changed') |
829 | -def loggregator_relation_joined(): |
830 | - config = hookenv.config() |
831 | - loggregator_address = hookenv.unit_get('private-address').encode('utf-8') |
832 | - hookenv.relation_set(None, { |
833 | - 'shared_secret': config['client_secret'], |
834 | - 'loggregator_address': loggregator_address}) |
835 | - |
836 | - |
837 | -@hooks.hook('nats-relation-changed') |
838 | -def nats_relation_changed(): |
839 | - services.reconfigure_services() |
840 | - |
841 | - |
842 | -if __name__ == '__main__': |
843 | - log("Running {} hook".format(sys.argv[0])) |
844 | - if hookenv.relation_id(): |
845 | - log("Relation {} with {}".format( |
846 | - hookenv.relation_id(), hookenv.remote_unit())) |
847 | - hooks.execute(sys.argv) |
848 | |
849 | === modified symlink 'hooks/loggregator-relation-changed' (properties changed: -x to +x) |
850 | === target was u'hooks.py' |
851 | --- hooks/loggregator-relation-changed 1970-01-01 00:00:00 +0000 |
852 | +++ hooks/loggregator-relation-changed 2014-05-29 18:29:18 +0000 |
853 | @@ -0,0 +1,8 @@ |
854 | +#!/usr/bin/env python |
855 | +from charmhelpers.core import hookenv |
856 | + |
857 | +config = hookenv.config() |
858 | +loggregator_address = hookenv.unit_get('private-address').encode('utf-8') |
859 | +hookenv.relation_set(None, { |
860 | + 'shared_secret': config['client_secret'], |
861 | + 'loggregator_address': loggregator_address}) |
862 | |
863 | === modified symlink 'hooks/nats-relation-changed' (properties changed: -x to +x) |
864 | === target was u'hooks.py' |
865 | --- hooks/nats-relation-changed 1970-01-01 00:00:00 +0000 |
866 | +++ hooks/nats-relation-changed 2014-05-29 18:29:18 +0000 |
867 | @@ -0,0 +1,5 @@ |
868 | +#!/usr/bin/env python |
869 | +from charmhelpers.core import services |
870 | +import config |
871 | +manager = services.ServiceManager(config.SERVICES) |
872 | +manager.manage() |
873 | |
874 | === removed symlink 'hooks/start' |
875 | === target was u'hooks.py' |
876 | === modified symlink 'hooks/stop' (properties changed: -x to +x) |
877 | === target was u'hooks.py' |
878 | --- hooks/stop 1970-01-01 00:00:00 +0000 |
879 | +++ hooks/stop 2014-05-29 18:29:18 +0000 |
880 | @@ -0,0 +1,5 @@ |
881 | +#!/usr/bin/env python |
882 | +from charmhelpers.core import services |
883 | +import config |
884 | +manager = services.ServiceManager(config.SERVICES) |
885 | +manager.manage() |
886 | |
887 | === modified symlink 'hooks/upgrade-charm' (properties changed: -x to +x) |
888 | === target was u'hooks.py' |
889 | --- hooks/upgrade-charm 1970-01-01 00:00:00 +0000 |
890 | +++ hooks/upgrade-charm 2014-05-29 18:29:18 +0000 |
891 | @@ -0,0 +1,5 @@ |
892 | +#!/usr/bin/env python |
893 | +from charmhelpers.core import services |
894 | +import config |
895 | +manager = services.ServiceManager(config.SERVICES) |
896 | +manager.manage() |
Reviewers: mp+221444_ code.launchpad. net,
Message:
Please take a look.
Description:
Refactor to callback services API
https:/ /code.launchpad .net/~johnsca/ charms/ trusty/ cf-loggregator/ services- callback- fu/+merge/ 221444
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/99640043/
Affected files (+455, -337 lines): ers/contrib/ cloudfoundry/ contexts. py ers/core/ host.py ers/core/ services. py ers/core/ templating. py
A [revision details]
M hooks/charmhelp
M hooks/charmhelp
M hooks/charmhelp
M hooks/charmhelp
M hooks/config.py
D hooks/hooks.py