Merge lp:~gnuoy/charms/trusty/ceph-radosgw/next-charmhelper-sync into lp:~openstack-charmers-archive/charms/trusty/ceph-radosgw/next
- Trusty Tahr (14.04)
- next-charmhelper-sync
- Merge into next
Proposed by
Liam Young
Status: | Merged |
---|---|
Merged at revision: | 26 |
Proposed branch: | lp:~gnuoy/charms/trusty/ceph-radosgw/next-charmhelper-sync |
Merge into: | lp:~openstack-charmers-archive/charms/trusty/ceph-radosgw/next |
Diff against target: |
934 lines (+672/-38) 11 files modified
.bzrignore (+1/-0) Makefile (+8/-2) hooks/charmhelpers/contrib/storage/linux/utils.py (+3/-0) hooks/charmhelpers/core/hookenv.py (+41/-16) hooks/charmhelpers/core/host.py (+36/-7) hooks/charmhelpers/core/services/__init__.py (+2/-0) hooks/charmhelpers/core/services/base.py (+313/-0) hooks/charmhelpers/core/services/helpers.py (+125/-0) hooks/charmhelpers/core/templating.py (+51/-0) hooks/charmhelpers/fetch/__init__.py (+51/-12) hooks/charmhelpers/fetch/archiveurl.py (+41/-1) |
To merge this branch: | bzr merge lp:~gnuoy/charms/trusty/ceph-radosgw/next-charmhelper-sync |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Chris Glass (community) | Approve | ||
Review via email:
|
Commit message
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file '.bzrignore' |
2 | --- .bzrignore 2012-10-08 15:58:16 +0000 |
3 | +++ .bzrignore 2014-09-17 16:10:13 +0000 |
4 | @@ -1,2 +1,3 @@ |
5 | .project |
6 | .pydevproject |
7 | +bin |
8 | |
9 | === modified file 'Makefile' |
10 | --- Makefile 2014-05-21 10:10:54 +0000 |
11 | +++ Makefile 2014-09-17 16:10:13 +0000 |
12 | @@ -1,11 +1,17 @@ |
13 | #!/usr/bin/make |
14 | +PYTHON := /usr/bin/env python |
15 | |
16 | lint: |
17 | @flake8 --exclude hooks/charmhelpers hooks |
18 | @charm proof |
19 | |
20 | -sync: |
21 | - @charm-helper-sync -c charm-helpers-sync.yaml |
22 | +bin/charm_helpers_sync.py: |
23 | + @mkdir -p bin |
24 | + @bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \ |
25 | + > bin/charm_helpers_sync.py |
26 | + |
27 | +sync: bin/charm_helpers_sync.py |
28 | + @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-sync.yaml |
29 | |
30 | publish: lint |
31 | bzr push lp:charms/ceph-radosgw |
32 | |
33 | === modified file 'hooks/charmhelpers/contrib/storage/linux/utils.py' |
34 | --- hooks/charmhelpers/contrib/storage/linux/utils.py 2014-07-24 09:43:27 +0000 |
35 | +++ hooks/charmhelpers/contrib/storage/linux/utils.py 2014-09-17 16:10:13 +0000 |
36 | @@ -46,5 +46,8 @@ |
37 | :returns: boolean: True if the path represents a mounted device, False if |
38 | it doesn't. |
39 | ''' |
40 | + is_partition = bool(re.search(r".*[0-9]+\b", device)) |
41 | out = check_output(['mount']) |
42 | + if is_partition: |
43 | + return bool(re.search(device + r"\b", out)) |
44 | return bool(re.search(device + r"[0-9]+\b", out)) |
45 | |
46 | === modified file 'hooks/charmhelpers/core/hookenv.py' |
47 | --- hooks/charmhelpers/core/hookenv.py 2014-07-24 09:43:27 +0000 |
48 | +++ hooks/charmhelpers/core/hookenv.py 2014-09-17 16:10:13 +0000 |
49 | @@ -156,12 +156,15 @@ |
50 | |
51 | |
52 | class Config(dict): |
53 | - """A Juju charm config dictionary that can write itself to |
54 | - disk (as json) and track which values have changed since |
55 | - the previous hook invocation. |
56 | - |
57 | - Do not instantiate this object directly - instead call |
58 | - ``hookenv.config()`` |
59 | + """A dictionary representation of the charm's config.yaml, with some |
60 | + extra features: |
61 | + |
62 | + - See which values in the dictionary have changed since the previous hook. |
63 | + - For values that have changed, see what the previous value was. |
64 | + - Store arbitrary data for use in a later hook. |
65 | + |
66 | + NOTE: Do not instantiate this object directly - instead call |
67 | + ``hookenv.config()``, which will return an instance of :class:`Config`. |
68 | |
69 | Example usage:: |
70 | |
71 | @@ -170,8 +173,8 @@ |
72 | >>> config = hookenv.config() |
73 | >>> config['foo'] |
74 | 'bar' |
75 | + >>> # store a new key/value for later use |
76 | >>> config['mykey'] = 'myval' |
77 | - >>> config.save() |
78 | |
79 | |
80 | >>> # user runs `juju set mycharm foo=baz` |
81 | @@ -188,22 +191,34 @@ |
82 | >>> # keys/values that we add are preserved across hooks |
83 | >>> config['mykey'] |
84 | 'myval' |
85 | - >>> # don't forget to save at the end of hook! |
86 | - >>> config.save() |
87 | |
88 | """ |
89 | CONFIG_FILE_NAME = '.juju-persistent-config' |
90 | |
91 | def __init__(self, *args, **kw): |
92 | super(Config, self).__init__(*args, **kw) |
93 | + self.implicit_save = True |
94 | self._prev_dict = None |
95 | self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME) |
96 | if os.path.exists(self.path): |
97 | self.load_previous() |
98 | |
99 | + def __getitem__(self, key): |
100 | + """For regular dict lookups, check the current juju config first, |
101 | + then the previous (saved) copy. This ensures that user-saved values |
102 | + will be returned by a dict lookup. |
103 | + |
104 | + """ |
105 | + try: |
106 | + return dict.__getitem__(self, key) |
107 | + except KeyError: |
108 | + return (self._prev_dict or {})[key] |
109 | + |
110 | def load_previous(self, path=None): |
111 | - """Load previous copy of config from disk so that current values |
112 | - can be compared to previous values. |
113 | + """Load previous copy of config from disk. |
114 | + |
115 | + In normal usage you don't need to call this method directly - it |
116 | + is called automatically at object initialization. |
117 | |
118 | :param path: |
119 | |
120 | @@ -218,8 +233,8 @@ |
121 | self._prev_dict = json.load(f) |
122 | |
123 | def changed(self, key): |
124 | - """Return true if the value for this key has changed since |
125 | - the last save. |
126 | + """Return True if the current value for this key is different from |
127 | + the previous value. |
128 | |
129 | """ |
130 | if self._prev_dict is None: |
131 | @@ -228,7 +243,7 @@ |
132 | |
133 | def previous(self, key): |
134 | """Return previous value for this key, or None if there |
135 | - is no "previous" value. |
136 | + is no previous value. |
137 | |
138 | """ |
139 | if self._prev_dict: |
140 | @@ -238,7 +253,13 @@ |
141 | def save(self): |
142 | """Save this config to disk. |
143 | |
144 | - Preserves items in _prev_dict that do not exist in self. |
145 | + If the charm is using the :mod:`Services Framework <services.base>` |
146 | + or :meth:'@hook <Hooks.hook>' decorator, this |
147 | + is called automatically at the end of successful hook execution. |
148 | + Otherwise, it should be called directly by user code. |
149 | + |
150 | + To disable automatic saves, set ``implicit_save=False`` on this |
151 | + instance. |
152 | |
153 | """ |
154 | if self._prev_dict: |
155 | @@ -285,8 +306,9 @@ |
156 | raise |
157 | |
158 | |
159 | -def relation_set(relation_id=None, relation_settings={}, **kwargs): |
160 | +def relation_set(relation_id=None, relation_settings=None, **kwargs): |
161 | """Set relation information for the current unit""" |
162 | + relation_settings = relation_settings if relation_settings else {} |
163 | relation_cmd_line = ['relation-set'] |
164 | if relation_id is not None: |
165 | relation_cmd_line.extend(('-r', relation_id)) |
166 | @@ -477,6 +499,9 @@ |
167 | hook_name = os.path.basename(args[0]) |
168 | if hook_name in self._hooks: |
169 | self._hooks[hook_name]() |
170 | + cfg = config() |
171 | + if cfg.implicit_save: |
172 | + cfg.save() |
173 | else: |
174 | raise UnregisteredHookError(hook_name) |
175 | |
176 | |
177 | === modified file 'hooks/charmhelpers/core/host.py' |
178 | --- hooks/charmhelpers/core/host.py 2014-07-24 09:43:27 +0000 |
179 | +++ hooks/charmhelpers/core/host.py 2014-09-17 16:10:13 +0000 |
180 | @@ -12,6 +12,8 @@ |
181 | import string |
182 | import subprocess |
183 | import hashlib |
184 | +import shutil |
185 | +from contextlib import contextmanager |
186 | |
187 | from collections import OrderedDict |
188 | |
189 | @@ -52,7 +54,7 @@ |
190 | def service_running(service): |
191 | """Determine whether a system service is running""" |
192 | try: |
193 | - output = subprocess.check_output(['service', service, 'status']) |
194 | + output = subprocess.check_output(['service', service, 'status'], stderr=subprocess.STDOUT) |
195 | except subprocess.CalledProcessError: |
196 | return False |
197 | else: |
198 | @@ -62,6 +64,16 @@ |
199 | return False |
200 | |
201 | |
202 | +def service_available(service_name): |
203 | + """Determine whether a system service is available""" |
204 | + try: |
205 | + subprocess.check_output(['service', service_name, 'status'], stderr=subprocess.STDOUT) |
206 | + except subprocess.CalledProcessError: |
207 | + return False |
208 | + else: |
209 | + return True |
210 | + |
211 | + |
212 | def adduser(username, password=None, shell='/bin/bash', system_user=False): |
213 | """Add a user to the system""" |
214 | try: |
215 | @@ -320,12 +332,29 @@ |
216 | |
217 | ''' |
218 | import apt_pkg |
219 | + from charmhelpers.fetch import apt_cache |
220 | if not pkgcache: |
221 | - apt_pkg.init() |
222 | - # Force Apt to build its cache in memory. That way we avoid race |
223 | - # conditions with other applications building the cache in the same |
224 | - # place. |
225 | - apt_pkg.config.set("Dir::Cache::pkgcache", "") |
226 | - pkgcache = apt_pkg.Cache() |
227 | + pkgcache = apt_cache() |
228 | pkg = pkgcache[package] |
229 | return apt_pkg.version_compare(pkg.current_ver.ver_str, revno) |
230 | + |
231 | + |
232 | +@contextmanager |
233 | +def chdir(d): |
234 | + cur = os.getcwd() |
235 | + try: |
236 | + yield os.chdir(d) |
237 | + finally: |
238 | + os.chdir(cur) |
239 | + |
240 | + |
241 | +def chownr(path, owner, group): |
242 | + uid = pwd.getpwnam(owner).pw_uid |
243 | + gid = grp.getgrnam(group).gr_gid |
244 | + |
245 | + for root, dirs, files in os.walk(path): |
246 | + for name in dirs + files: |
247 | + full = os.path.join(root, name) |
248 | + broken_symlink = os.path.lexists(full) and not os.path.exists(full) |
249 | + if not broken_symlink: |
250 | + os.chown(full, uid, gid) |
251 | |
252 | === added directory 'hooks/charmhelpers/core/services' |
253 | === added file 'hooks/charmhelpers/core/services/__init__.py' |
254 | --- hooks/charmhelpers/core/services/__init__.py 1970-01-01 00:00:00 +0000 |
255 | +++ hooks/charmhelpers/core/services/__init__.py 2014-09-17 16:10:13 +0000 |
256 | @@ -0,0 +1,2 @@ |
257 | +from .base import * |
258 | +from .helpers import * |
259 | |
260 | === added file 'hooks/charmhelpers/core/services/base.py' |
261 | --- hooks/charmhelpers/core/services/base.py 1970-01-01 00:00:00 +0000 |
262 | +++ hooks/charmhelpers/core/services/base.py 2014-09-17 16:10:13 +0000 |
263 | @@ -0,0 +1,313 @@ |
264 | +import os |
265 | +import re |
266 | +import json |
267 | +from collections import Iterable |
268 | + |
269 | +from charmhelpers.core import host |
270 | +from charmhelpers.core import hookenv |
271 | + |
272 | + |
273 | +__all__ = ['ServiceManager', 'ManagerCallback', |
274 | + 'PortManagerCallback', 'open_ports', 'close_ports', 'manage_ports', |
275 | + 'service_restart', 'service_stop'] |
276 | + |
277 | + |
278 | +class ServiceManager(object): |
279 | + def __init__(self, services=None): |
280 | + """ |
281 | + Register a list of services, given their definitions. |
282 | + |
283 | + Service definitions are dicts in the following formats (all keys except |
284 | + 'service' are optional):: |
285 | + |
286 | + { |
287 | + "service": <service name>, |
288 | + "required_data": <list of required data contexts>, |
289 | + "provided_data": <list of provided data contexts>, |
290 | + "data_ready": <one or more callbacks>, |
291 | + "data_lost": <one or more callbacks>, |
292 | + "start": <one or more callbacks>, |
293 | + "stop": <one or more callbacks>, |
294 | + "ports": <list of ports to manage>, |
295 | + } |
296 | + |
297 | + The 'required_data' list should contain dicts of required data (or |
298 | + dependency managers that act like dicts and know how to collect the data). |
299 | + Only when all items in the 'required_data' list are populated are the list |
300 | + of 'data_ready' and 'start' callbacks executed. See `is_ready()` for more |
301 | + information. |
302 | + |
303 | + The 'provided_data' list should contain relation data providers, most likely |
304 | + a subclass of :class:`charmhelpers.core.services.helpers.RelationContext`, |
305 | + that will indicate a set of data to set on a given relation. |
306 | + |
307 | + The 'data_ready' value should be either a single callback, or a list of |
308 | + callbacks, to be called when all items in 'required_data' pass `is_ready()`. |
309 | + Each callback will be called with the service name as the only parameter. |
310 | + After all of the 'data_ready' callbacks are called, the 'start' callbacks |
311 | + are fired. |
312 | + |
313 | + The 'data_lost' value should be either a single callback, or a list of |
314 | + callbacks, to be called when a 'required_data' item no longer passes |
315 | + `is_ready()`. Each callback will be called with the service name as the |
316 | + only parameter. After all of the 'data_lost' callbacks are called, |
317 | + the 'stop' callbacks are fired. |
318 | + |
319 | + The 'start' value should be either a single callback, or a list of |
320 | + callbacks, to be called when starting the service, after the 'data_ready' |
321 | + callbacks are complete. Each callback will be called with the service |
322 | + name as the only parameter. This defaults to |
323 | + `[host.service_start, services.open_ports]`. |
324 | + |
325 | + The 'stop' value should be either a single callback, or a list of |
326 | + callbacks, to be called when stopping the service. If the service is |
327 | + being stopped because it no longer has all of its 'required_data', this |
328 | + will be called after all of the 'data_lost' callbacks are complete. |
329 | + Each callback will be called with the service name as the only parameter. |
330 | + This defaults to `[services.close_ports, host.service_stop]`. |
331 | + |
332 | + The 'ports' value should be a list of ports to manage. The default |
333 | + 'start' handler will open the ports after the service is started, |
334 | + and the default 'stop' handler will close the ports prior to stopping |
335 | + the service. |
336 | + |
337 | + |
338 | + Examples: |
339 | + |
340 | + The following registers an Upstart service called bingod that depends on |
341 | + a mongodb relation and which runs a custom `db_migrate` function prior to |
342 | + restarting the service, and a Runit service called spadesd:: |
343 | + |
344 | + manager = services.ServiceManager([ |
345 | + { |
346 | + 'service': 'bingod', |
347 | + 'ports': [80, 443], |
348 | + 'required_data': [MongoRelation(), config(), {'my': 'data'}], |
349 | + 'data_ready': [ |
350 | + services.template(source='bingod.conf'), |
351 | + services.template(source='bingod.ini', |
352 | + target='/etc/bingod.ini', |
353 | + owner='bingo', perms=0400), |
354 | + ], |
355 | + }, |
356 | + { |
357 | + 'service': 'spadesd', |
358 | + 'data_ready': services.template(source='spadesd_run.j2', |
359 | + target='/etc/sv/spadesd/run', |
360 | + perms=0555), |
361 | + 'start': runit_start, |
362 | + 'stop': runit_stop, |
363 | + }, |
364 | + ]) |
365 | + manager.manage() |
366 | + """ |
367 | + self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json') |
368 | + self._ready = None |
369 | + self.services = {} |
370 | + for service in services or []: |
371 | + service_name = service['service'] |
372 | + self.services[service_name] = service |
373 | + |
374 | + def manage(self): |
375 | + """ |
376 | + Handle the current hook by doing The Right Thing with the registered services. |
377 | + """ |
378 | + hook_name = hookenv.hook_name() |
379 | + if hook_name == 'stop': |
380 | + self.stop_services() |
381 | + else: |
382 | + self.provide_data() |
383 | + self.reconfigure_services() |
384 | + cfg = hookenv.config() |
385 | + if cfg.implicit_save: |
386 | + cfg.save() |
387 | + |
388 | + def provide_data(self): |
389 | + """ |
390 | + Set the relation data for each provider in the ``provided_data`` list. |
391 | + |
392 | + A provider must have a `name` attribute, which indicates which relation |
393 | + to set data on, and a `provide_data()` method, which returns a dict of |
394 | + data to set. |
395 | + """ |
396 | + hook_name = hookenv.hook_name() |
397 | + for service in self.services.values(): |
398 | + for provider in service.get('provided_data', []): |
399 | + if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name): |
400 | + data = provider.provide_data() |
401 | + _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data |
402 | + if _ready: |
403 | + hookenv.relation_set(None, data) |
404 | + |
405 | + def reconfigure_services(self, *service_names): |
406 | + """ |
407 | + Update all files for one or more registered services, and, |
408 | + if ready, optionally restart them. |
409 | + |
410 | + If no service names are given, reconfigures all registered services. |
411 | + """ |
412 | + for service_name in service_names or self.services.keys(): |
413 | + if self.is_ready(service_name): |
414 | + self.fire_event('data_ready', service_name) |
415 | + self.fire_event('start', service_name, default=[ |
416 | + service_restart, |
417 | + manage_ports]) |
418 | + self.save_ready(service_name) |
419 | + else: |
420 | + if self.was_ready(service_name): |
421 | + self.fire_event('data_lost', service_name) |
422 | + self.fire_event('stop', service_name, default=[ |
423 | + manage_ports, |
424 | + service_stop]) |
425 | + self.save_lost(service_name) |
426 | + |
427 | + def stop_services(self, *service_names): |
428 | + """ |
429 | + Stop one or more registered services, by name. |
430 | + |
431 | + If no service names are given, stops all registered services. |
432 | + """ |
433 | + for service_name in service_names or self.services.keys(): |
434 | + self.fire_event('stop', service_name, default=[ |
435 | + manage_ports, |
436 | + service_stop]) |
437 | + |
438 | + def get_service(self, service_name): |
439 | + """ |
440 | + Given the name of a registered service, return its service definition. |
441 | + """ |
442 | + service = self.services.get(service_name) |
443 | + if not service: |
444 | + raise KeyError('Service not registered: %s' % service_name) |
445 | + return service |
446 | + |
447 | + def fire_event(self, event_name, service_name, default=None): |
448 | + """ |
449 | + Fire a data_ready, data_lost, start, or stop event on a given service. |
450 | + """ |
451 | + service = self.get_service(service_name) |
452 | + callbacks = service.get(event_name, default) |
453 | + if not callbacks: |
454 | + return |
455 | + if not isinstance(callbacks, Iterable): |
456 | + callbacks = [callbacks] |
457 | + for callback in callbacks: |
458 | + if isinstance(callback, ManagerCallback): |
459 | + callback(self, service_name, event_name) |
460 | + else: |
461 | + callback(service_name) |
462 | + |
463 | + def is_ready(self, service_name): |
464 | + """ |
465 | + Determine if a registered service is ready, by checking its 'required_data'. |
466 | + |
467 | + A 'required_data' item can be any mapping type, and is considered ready |
468 | + if `bool(item)` evaluates as True. |
469 | + """ |
470 | + service = self.get_service(service_name) |
471 | + reqs = service.get('required_data', []) |
472 | + return all(bool(req) for req in reqs) |
473 | + |
474 | + def _load_ready_file(self): |
475 | + if self._ready is not None: |
476 | + return |
477 | + if os.path.exists(self._ready_file): |
478 | + with open(self._ready_file) as fp: |
479 | + self._ready = set(json.load(fp)) |
480 | + else: |
481 | + self._ready = set() |
482 | + |
483 | + def _save_ready_file(self): |
484 | + if self._ready is None: |
485 | + return |
486 | + with open(self._ready_file, 'w') as fp: |
487 | + json.dump(list(self._ready), fp) |
488 | + |
489 | + def save_ready(self, service_name): |
490 | + """ |
491 | + Save an indicator that the given service is now data_ready. |
492 | + """ |
493 | + self._load_ready_file() |
494 | + self._ready.add(service_name) |
495 | + self._save_ready_file() |
496 | + |
497 | + def save_lost(self, service_name): |
498 | + """ |
499 | + Save an indicator that the given service is no longer data_ready. |
500 | + """ |
501 | + self._load_ready_file() |
502 | + self._ready.discard(service_name) |
503 | + self._save_ready_file() |
504 | + |
505 | + def was_ready(self, service_name): |
506 | + """ |
507 | + Determine if the given service was previously data_ready. |
508 | + """ |
509 | + self._load_ready_file() |
510 | + return service_name in self._ready |
511 | + |
512 | + |
513 | +class ManagerCallback(object): |
514 | + """ |
515 | + Special case of a callback that takes the `ServiceManager` instance |
516 | + in addition to the service name. |
517 | + |
518 | + Subclasses should implement `__call__` which should accept three parameters: |
519 | + |
520 | + * `manager` The `ServiceManager` instance |
521 | + * `service_name` The name of the service it's being triggered for |
522 | + * `event_name` The name of the event that this callback is handling |
523 | + """ |
524 | + def __call__(self, manager, service_name, event_name): |
525 | + raise NotImplementedError() |
526 | + |
527 | + |
528 | +class PortManagerCallback(ManagerCallback): |
529 | + """ |
530 | + Callback class that will open or close ports, for use as either |
531 | + a start or stop action. |
532 | + """ |
533 | + def __call__(self, manager, service_name, event_name): |
534 | + service = manager.get_service(service_name) |
535 | + new_ports = service.get('ports', []) |
536 | + port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name)) |
537 | + if os.path.exists(port_file): |
538 | + with open(port_file) as fp: |
539 | + old_ports = fp.read().split(',') |
540 | + for old_port in old_ports: |
541 | + if bool(old_port): |
542 | + old_port = int(old_port) |
543 | + if old_port not in new_ports: |
544 | + hookenv.close_port(old_port) |
545 | + with open(port_file, 'w') as fp: |
546 | + fp.write(','.join(str(port) for port in new_ports)) |
547 | + for port in new_ports: |
548 | + if event_name == 'start': |
549 | + hookenv.open_port(port) |
550 | + elif event_name == 'stop': |
551 | + hookenv.close_port(port) |
552 | + |
553 | + |
554 | +def service_stop(service_name): |
555 | + """ |
556 | + Wrapper around host.service_stop to prevent spurious "unknown service" |
557 | + messages in the logs. |
558 | + """ |
559 | + if host.service_running(service_name): |
560 | + host.service_stop(service_name) |
561 | + |
562 | + |
563 | +def service_restart(service_name): |
564 | + """ |
565 | + Wrapper around host.service_restart to prevent spurious "unknown service" |
566 | + messages in the logs. |
567 | + """ |
568 | + if host.service_available(service_name): |
569 | + if host.service_running(service_name): |
570 | + host.service_restart(service_name) |
571 | + else: |
572 | + host.service_start(service_name) |
573 | + |
574 | + |
575 | +# Convenience aliases |
576 | +open_ports = close_ports = manage_ports = PortManagerCallback() |
577 | |
578 | === added file 'hooks/charmhelpers/core/services/helpers.py' |
579 | --- hooks/charmhelpers/core/services/helpers.py 1970-01-01 00:00:00 +0000 |
580 | +++ hooks/charmhelpers/core/services/helpers.py 2014-09-17 16:10:13 +0000 |
581 | @@ -0,0 +1,125 @@ |
582 | +from charmhelpers.core import hookenv |
583 | +from charmhelpers.core import templating |
584 | + |
585 | +from charmhelpers.core.services.base import ManagerCallback |
586 | + |
587 | + |
588 | +__all__ = ['RelationContext', 'TemplateCallback', |
589 | + 'render_template', 'template'] |
590 | + |
591 | + |
592 | +class RelationContext(dict): |
593 | + """ |
594 | + Base class for a context generator that gets relation data from juju. |
595 | + |
596 | + Subclasses must provide the attributes `name`, which is the name of the |
597 | + interface of interest, `interface`, which is the type of the interface of |
598 | + interest, and `required_keys`, which is the set of keys required for the |
599 | + relation to be considered complete. The data for all interfaces matching |
600 | + the `name` attribute that are complete will used to populate the dictionary |
601 | + values (see `get_data`, below). |
602 | + |
603 | + The generated context will be namespaced under the interface type, to prevent |
604 | + potential naming conflicts. |
605 | + """ |
606 | + name = None |
607 | + interface = None |
608 | + required_keys = [] |
609 | + |
610 | + def __init__(self, *args, **kwargs): |
611 | + super(RelationContext, self).__init__(*args, **kwargs) |
612 | + self.get_data() |
613 | + |
614 | + def __bool__(self): |
615 | + """ |
616 | + Returns True if all of the required_keys are available. |
617 | + """ |
618 | + return self.is_ready() |
619 | + |
620 | + __nonzero__ = __bool__ |
621 | + |
622 | + def __repr__(self): |
623 | + return super(RelationContext, self).__repr__() |
624 | + |
625 | + def is_ready(self): |
626 | + """ |
627 | + Returns True if all of the `required_keys` are available from any units. |
628 | + """ |
629 | + ready = len(self.get(self.name, [])) > 0 |
630 | + if not ready: |
631 | + hookenv.log('Incomplete relation: {}'.format(self.__class__.__name__), hookenv.DEBUG) |
632 | + return ready |
633 | + |
634 | + def _is_ready(self, unit_data): |
635 | + """ |
636 | + Helper method that tests a set of relation data and returns True if |
637 | + all of the `required_keys` are present. |
638 | + """ |
639 | + return set(unit_data.keys()).issuperset(set(self.required_keys)) |
640 | + |
641 | + def get_data(self): |
642 | + """ |
643 | + Retrieve the relation data for each unit involved in a relation and, |
644 | + if complete, store it in a list under `self[self.name]`. This |
645 | + is automatically called when the RelationContext is instantiated. |
646 | + |
647 | + The units are sorted lexographically first by the service ID, then by |
648 | + the unit ID. Thus, if an interface has two other services, 'db:1' |
649 | + and 'db:2', with 'db:1' having two units, 'wordpress/0' and 'wordpress/1', |
650 | + and 'db:2' having one unit, 'mediawiki/0', all of which have a complete |
651 | + set of data, the relation data for the units will be stored in the |
652 | + order: 'wordpress/0', 'wordpress/1', 'mediawiki/0'. |
653 | + |
654 | + If you only care about a single unit on the relation, you can just |
655 | + access it as `{{ interface[0]['key'] }}`. However, if you can at all |
656 | + support multiple units on a relation, you should iterate over the list, |
657 | + like:: |
658 | + |
659 | + {% for unit in interface -%} |
660 | + {{ unit['key'] }}{% if not loop.last %},{% endif %} |
661 | + {%- endfor %} |
662 | + |
663 | + Note that since all sets of relation data from all related services and |
664 | + units are in a single list, if you need to know which service or unit a |
665 | + set of data came from, you'll need to extend this class to preserve |
666 | + that information. |
667 | + """ |
668 | + if not hookenv.relation_ids(self.name): |
669 | + return |
670 | + |
671 | + ns = self.setdefault(self.name, []) |
672 | + for rid in sorted(hookenv.relation_ids(self.name)): |
673 | + for unit in sorted(hookenv.related_units(rid)): |
674 | + reldata = hookenv.relation_get(rid=rid, unit=unit) |
675 | + if self._is_ready(reldata): |
676 | + ns.append(reldata) |
677 | + |
678 | + def provide_data(self): |
679 | + """ |
680 | + Return data to be relation_set for this interface. |
681 | + """ |
682 | + return {} |
683 | + |
684 | + |
685 | +class TemplateCallback(ManagerCallback): |
686 | + """ |
687 | + Callback class that will render a template, for use as a ready action. |
688 | + """ |
689 | + def __init__(self, source, target, owner='root', group='root', perms=0444): |
690 | + self.source = source |
691 | + self.target = target |
692 | + self.owner = owner |
693 | + self.group = group |
694 | + self.perms = perms |
695 | + |
696 | + def __call__(self, manager, service_name, event_name): |
697 | + service = manager.get_service(service_name) |
698 | + context = {} |
699 | + for ctx in service.get('required_data', []): |
700 | + context.update(ctx) |
701 | + templating.render(self.source, self.target, context, |
702 | + self.owner, self.group, self.perms) |
703 | + |
704 | + |
705 | +# Convenience aliases for templates |
706 | +render_template = template = TemplateCallback |
707 | |
708 | === added file 'hooks/charmhelpers/core/templating.py' |
709 | --- hooks/charmhelpers/core/templating.py 1970-01-01 00:00:00 +0000 |
710 | +++ hooks/charmhelpers/core/templating.py 2014-09-17 16:10:13 +0000 |
711 | @@ -0,0 +1,51 @@ |
712 | +import os |
713 | + |
714 | +from charmhelpers.core import host |
715 | +from charmhelpers.core import hookenv |
716 | + |
717 | + |
718 | +def render(source, target, context, owner='root', group='root', perms=0444, templates_dir=None): |
719 | + """ |
720 | + Render a template. |
721 | + |
722 | + The `source` path, if not absolute, is relative to the `templates_dir`. |
723 | + |
724 | + The `target` path should be absolute. |
725 | + |
726 | + The context should be a dict containing the values to be replaced in the |
727 | + template. |
728 | + |
729 | + The `owner`, `group`, and `perms` options will be passed to `write_file`. |
730 | + |
731 | + If omitted, `templates_dir` defaults to the `templates` folder in the charm. |
732 | + |
733 | + Note: Using this requires python-jinja2; if it is not installed, calling |
734 | + this will attempt to use charmhelpers.fetch.apt_install to install it. |
735 | + """ |
736 | + try: |
737 | + from jinja2 import FileSystemLoader, Environment, exceptions |
738 | + except ImportError: |
739 | + try: |
740 | + from charmhelpers.fetch import apt_install |
741 | + except ImportError: |
742 | + hookenv.log('Could not import jinja2, and could not import ' |
743 | + 'charmhelpers.fetch to install it', |
744 | + level=hookenv.ERROR) |
745 | + raise |
746 | + apt_install('python-jinja2', fatal=True) |
747 | + from jinja2 import FileSystemLoader, Environment, exceptions |
748 | + |
749 | + if templates_dir is None: |
750 | + templates_dir = os.path.join(hookenv.charm_dir(), 'templates') |
751 | + loader = Environment(loader=FileSystemLoader(templates_dir)) |
752 | + try: |
753 | + source = source |
754 | + template = loader.get_template(source) |
755 | + except exceptions.TemplateNotFound as e: |
756 | + hookenv.log('Could not load template %s from %s.' % |
757 | + (source, templates_dir), |
758 | + level=hookenv.ERROR) |
759 | + raise e |
760 | + content = template.render(context) |
761 | + host.mkdir(os.path.dirname(target)) |
762 | + host.write_file(target, content, owner, group, perms) |
763 | |
764 | === modified file 'hooks/charmhelpers/fetch/__init__.py' |
765 | --- hooks/charmhelpers/fetch/__init__.py 2014-07-24 09:43:27 +0000 |
766 | +++ hooks/charmhelpers/fetch/__init__.py 2014-09-17 16:10:13 +0000 |
767 | @@ -1,4 +1,5 @@ |
768 | import importlib |
769 | +from tempfile import NamedTemporaryFile |
770 | import time |
771 | from yaml import safe_load |
772 | from charmhelpers.core.host import ( |
773 | @@ -116,14 +117,7 @@ |
774 | |
775 | def filter_installed_packages(packages): |
776 | """Returns a list of packages that require installation""" |
777 | - import apt_pkg |
778 | - apt_pkg.init() |
779 | - |
780 | - # Tell apt to build an in-memory cache to prevent race conditions (if |
781 | - # another process is already building the cache). |
782 | - apt_pkg.config.set("Dir::Cache::pkgcache", "") |
783 | - |
784 | - cache = apt_pkg.Cache() |
785 | + cache = apt_cache() |
786 | _pkgs = [] |
787 | for package in packages: |
788 | try: |
789 | @@ -136,6 +130,16 @@ |
790 | return _pkgs |
791 | |
792 | |
793 | +def apt_cache(in_memory=True): |
794 | + """Build and return an apt cache""" |
795 | + import apt_pkg |
796 | + apt_pkg.init() |
797 | + if in_memory: |
798 | + apt_pkg.config.set("Dir::Cache::pkgcache", "") |
799 | + apt_pkg.config.set("Dir::Cache::srcpkgcache", "") |
800 | + return apt_pkg.Cache() |
801 | + |
802 | + |
803 | def apt_install(packages, options=None, fatal=False): |
804 | """Install one or more packages""" |
805 | if options is None: |
806 | @@ -201,6 +205,27 @@ |
807 | |
808 | |
809 | def add_source(source, key=None): |
810 | + """Add a package source to this system. |
811 | + |
812 | + @param source: a URL or sources.list entry, as supported by |
813 | + add-apt-repository(1). Examples: |
814 | + ppa:charmers/example |
815 | + deb https://stub:key@private.example.com/ubuntu trusty main |
816 | + |
817 | + In addition: |
818 | + 'proposed:' may be used to enable the standard 'proposed' |
819 | + pocket for the release. |
820 | + 'cloud:' may be used to activate official cloud archive pockets, |
821 | + such as 'cloud:icehouse' |
822 | + |
823 | + @param key: A key to be added to the system's APT keyring and used |
824 | + to verify the signatures on packages. Ideally, this should be an |
825 | + ASCII format GPG public key including the block headers. A GPG key |
826 | + id may also be used, but be aware that only insecure protocols are |
827 | + available to retrieve the actual public key from a public keyserver |
828 | + placing your Juju environment at risk. ppa and cloud archive keys |
829 | + are securely added automtically, so sould not be provided. |
830 | + """ |
831 | if source is None: |
832 | log('Source is not present. Skipping') |
833 | return |
834 | @@ -225,10 +250,23 @@ |
835 | release = lsb_release()['DISTRIB_CODENAME'] |
836 | with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt: |
837 | apt.write(PROPOSED_POCKET.format(release)) |
838 | + else: |
839 | + raise SourceConfigError("Unknown source: {!r}".format(source)) |
840 | + |
841 | if key: |
842 | - subprocess.check_call(['apt-key', 'adv', '--keyserver', |
843 | - 'hkp://keyserver.ubuntu.com:80', '--recv', |
844 | - key]) |
845 | + if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key: |
846 | + with NamedTemporaryFile() as key_file: |
847 | + key_file.write(key) |
848 | + key_file.flush() |
849 | + key_file.seek(0) |
850 | + subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file) |
851 | + else: |
852 | + # Note that hkp: is in no way a secure protocol. Using a |
853 | + # GPG key id is pointless from a security POV unless you |
854 | + # absolutely trust your network and DNS. |
855 | + subprocess.check_call(['apt-key', 'adv', '--keyserver', |
856 | + 'hkp://keyserver.ubuntu.com:80', '--recv', |
857 | + key]) |
858 | |
859 | |
860 | def configure_sources(update=False, |
861 | @@ -238,7 +276,8 @@ |
862 | Configure multiple sources from charm configuration. |
863 | |
864 | The lists are encoded as yaml fragments in the configuration. |
865 | - The frament needs to be included as a string. |
866 | + The frament needs to be included as a string. Sources and their |
867 | + corresponding keys are of the types supported by add_source(). |
868 | |
869 | Example config: |
870 | install_sources: | |
871 | |
872 | === modified file 'hooks/charmhelpers/fetch/archiveurl.py' |
873 | --- hooks/charmhelpers/fetch/archiveurl.py 2014-03-25 12:26:40 +0000 |
874 | +++ hooks/charmhelpers/fetch/archiveurl.py 2014-09-17 16:10:13 +0000 |
875 | @@ -1,6 +1,8 @@ |
876 | import os |
877 | import urllib2 |
878 | +from urllib import urlretrieve |
879 | import urlparse |
880 | +import hashlib |
881 | |
882 | from charmhelpers.fetch import ( |
883 | BaseFetchHandler, |
884 | @@ -12,7 +14,17 @@ |
885 | ) |
886 | from charmhelpers.core.host import mkdir |
887 | |
888 | - |
889 | +""" |
890 | +This class is a plugin for charmhelpers.fetch.install_remote. |
891 | + |
892 | +It grabs, validates and installs remote archives fetched over "http", "https", "ftp" or "file" protocols. The contents of the archive are installed in $CHARM_DIR/fetched/. |
893 | + |
894 | +Example usage: |
895 | +install_remote("https://example.com/some/archive.tar.gz") |
896 | +# Installs the contents of archive.tar.gz in $CHARM_DIR/fetched/. |
897 | + |
898 | +See charmhelpers.fetch.archiveurl.get_archivehandler for supported archive types. |
899 | +""" |
900 | class ArchiveUrlFetchHandler(BaseFetchHandler): |
901 | """Handler for archives via generic URLs""" |
902 | def can_handle(self, source): |
903 | @@ -61,3 +73,31 @@ |
904 | except OSError as e: |
905 | raise UnhandledSource(e.strerror) |
906 | return extract(dld_file) |
907 | + |
908 | + # Mandatory file validation via Sha1 or MD5 hashing. |
909 | + def download_and_validate(self, url, hashsum, validate="sha1"): |
910 | + if validate == 'sha1' and len(hashsum) != 40: |
911 | + raise ValueError("HashSum must be = 40 characters when using sha1" |
912 | + " validation") |
913 | + if validate == 'md5' and len(hashsum) != 32: |
914 | + raise ValueError("HashSum must be = 32 characters when using md5" |
915 | + " validation") |
916 | + tempfile, headers = urlretrieve(url) |
917 | + self.validate_file(tempfile, hashsum, validate) |
918 | + return tempfile |
919 | + |
920 | + # Predicate method that returns status of hash matching expected hash. |
921 | + def validate_file(self, source, hashsum, vmethod='sha1'): |
922 | + if vmethod != 'sha1' and vmethod != 'md5': |
923 | + raise ValueError("Validation Method not supported") |
924 | + |
925 | + if vmethod == 'md5': |
926 | + m = hashlib.md5() |
927 | + if vmethod == 'sha1': |
928 | + m = hashlib.sha1() |
929 | + with open(source) as f: |
930 | + for line in f: |
931 | + m.update(line) |
932 | + if hashsum != m.hexdigest(): |
933 | + msg = "Hash Mismatch on {} expected {} got {}" |
934 | + raise ValueError(msg.format(source, hashsum, m.hexdigest())) |
Looks good! +1