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