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

Proposed by Cory Johns
Status: Merged
Merged at revision: 28
Proposed branch: lp:~johnsca/charms/trusty/cf-go-router/services-callback-fu
Merge into: lp:~cf-charmers/charms/trusty/cf-go-router/trunk
Diff against target: 946 lines (+474/-351)
11 files modified
config.yaml (+5/-2)
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 (+11/-0)
hooks/config.py (+41/-0)
hooks/hooks.py (+0/-93)
hooks/nats-relation-changed (+6/-0)
hooks/router-relation-joined (+5/-0)
hooks/stop (+6/-0)
To merge this branch: bzr merge lp:~johnsca/charms/trusty/cf-go-router/services-callback-fu
Reviewer Review Type Date Requested Status
Cloud Foundry Charmers Pending
Review via email: mp+221443@code.launchpad.net

Description of the change

Refactor to callback services API

https://codereview.appspot.com/104720044/

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

Reviewers: mp+221443_code.launchpad.net,

Message:
Please take a look.

Description:
Refactor to callback services API

https://code.launchpad.net/~johnsca/charms/trusty/cf-go-router/services-callback-fu/+merge/221443

(do not edit description out of merge proposal)

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

Affected files (+480, -355 lines):
   A [revision details]
   M config.yaml
   M hooks/charmhelpers/contrib/cloudfoundry/contexts.py
   M hooks/charmhelpers/core/host.py
   M hooks/charmhelpers/core/services.py
   M hooks/charmhelpers/core/templating.py
   M hooks/config.py
   D hooks/hooks.py

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

LGTM, thanks

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

https://codereview.appspot.com/104720044/diff/1/hooks/config.py#newcode23
hooks/config.py:23: result = subprocess.check_output(['dig', '+short',
'@8.8.8.8', address])
thank you google, there should be a better way, but I've done this too

https://codereview.appspot.com/104720044/diff/1/hooks/config.py#newcode38
hooks/config.py:38: 'ports': [80, 443],
we still expose 443 but its aspirational iirc

https://codereview.appspot.com/104720044/

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

On 2014/05/29 18:51:20, benjamin.saller wrote:
> hooks/config.py:23: result = subprocess.check_output(['dig', '+short',
> mailto:, address])
> thank you google, there should be a better way, but I've done this too

> hooks/config.py:38: 'ports': [80, 443],
> we still expose 443 but its aspirational iirc

Yeah, I just moved the existing logic, but it did seem like a strange
approach to me. I expected juju to provide a way to fetch the IP
address instead of the resolved name.

https://codereview.appspot.com/104720044/

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

*** Submitted:

Refactor to callback services API

R=benjamin.saller
CC=
https://codereview.appspot.com/104720044

https://codereview.appspot.com/104720044/

Preview Diff

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

Subscribers

People subscribed via source and target branches