Merge lp:~gnuoy/charms/trusty/ceph-radosgw/next-charmhelper-sync into lp:~openstack-charmers-archive/charms/trusty/ceph-radosgw/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
Reviewer Review Type Date Requested Status
Chris Glass (community) Approve
Review via email: mp+235011@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Chris Glass (tribaal) wrote :

Looks good! +1

review: Approve

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()))

Subscribers

People subscribed via source and target branches