Merge lp:~hopem/charms/trusty/ceph-osd/support-ipv6 into lp:~openstack-charmers-archive/charms/trusty/ceph-osd/next

Proposed by Edward Hope-Morley
Status: Merged
Merged at revision: 28
Proposed branch: lp:~hopem/charms/trusty/ceph-osd/support-ipv6
Merge into: lp:~openstack-charmers-archive/charms/trusty/ceph-osd/next
Diff against target: 1175 lines (+824/-40)
14 files modified
config.yaml (+3/-0)
hooks/charmhelpers/contrib/network/ip.py (+102/-0)
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)
hooks/hooks.py (+42/-4)
hooks/utils.py (+11/-0)
templates/ceph.conf (+3/-0)
To merge this branch: bzr merge lp:~hopem/charms/trusty/ceph-osd/support-ipv6
Reviewer Review Type Date Requested Status
Xiang Hui Pending
OpenStack Charmers Pending
Review via email: mp+235189@code.launchpad.net
To post a comment you must log in.
28. By Edward Hope-Morley

synced charm-helpers

29. By Edward Hope-Morley

fixed get_mon_hosts()

30. By Edward Hope-Morley

fixed get_mon_hosts()

31. By Edward Hope-Morley

synced ~xianghui/charm-helpers/format-ipv6

32. By Edward Hope-Morley

adjuested to new get_ipv6_addr

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'config.yaml'
2--- config.yaml 2014-08-15 09:06:49 +0000
3+++ config.yaml 2014-09-21 19:34:01 +0000
4@@ -103,3 +103,6 @@
5 description: |
6 The IP address and netmask of the cluster (back-side) network (e.g.,
7 192.168.0.0/24)
8+ prefer-ipv6:
9+ type: boolean
10+ default: False
11\ No newline at end of file
12
13=== modified file 'hooks/charmhelpers/contrib/network/ip.py'
14--- hooks/charmhelpers/contrib/network/ip.py 2014-07-25 08:07:41 +0000
15+++ hooks/charmhelpers/contrib/network/ip.py 2014-09-21 19:34:01 +0000
16@@ -1,3 +1,4 @@
17+import glob
18 import sys
19
20 from functools import partial
21@@ -154,3 +155,104 @@
22 get_iface_for_address = partial(_get_for_address, key='iface')
23
24 get_netmask_for_address = partial(_get_for_address, key='netmask')
25+
26+
27+def format_ipv6_addr(address):
28+ """
29+ IPv6 needs to be wrapped with [] in url link to parse correctly.
30+ """
31+ if is_ipv6(address):
32+ address = "[%s]" % address
33+ else:
34+ log("Not an valid ipv6 address: %s" % address,
35+ level=ERROR)
36+ address = None
37+ return address
38+
39+
40+def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False, fatal=True, exc_list=None):
41+ """
42+ Return the assigned IP address for a given interface, if any, or [].
43+ """
44+ # Extract nic if passed /dev/ethX
45+ if '/' in iface:
46+ iface = iface.split('/')[-1]
47+ if not exc_list:
48+ exc_list = []
49+ try:
50+ inet_num = getattr(netifaces, inet_type)
51+ except AttributeError:
52+ raise Exception('Unknown inet type ' + str(inet_type))
53+
54+ interfaces = netifaces.interfaces()
55+ if inc_aliases:
56+ ifaces = []
57+ for _iface in interfaces:
58+ if iface == _iface or _iface.split(':')[0] == iface:
59+ ifaces.append(_iface)
60+ if fatal and not ifaces:
61+ raise Exception("Invalid interface '%s'" % iface)
62+ ifaces.sort()
63+ else:
64+ if iface not in interfaces:
65+ if fatal:
66+ raise Exception("%s not found " % (iface))
67+ else:
68+ return []
69+ else:
70+ ifaces = [iface]
71+
72+ addresses = []
73+ for netiface in ifaces:
74+ net_info = netifaces.ifaddresses(netiface)
75+ if inet_num in net_info:
76+ for entry in net_info[inet_num]:
77+ if 'addr' in entry and entry['addr'] not in exc_list:
78+ addresses.append(entry['addr'])
79+ if fatal and not addresses:
80+ raise Exception("Interface '%s' doesn't have any %s addresses." % (iface, inet_type))
81+ return addresses
82+
83+get_ipv4_addr = partial(get_iface_addr, inet_type='AF_INET')
84+
85+
86+def get_ipv6_addr(iface='eth0', inc_aliases=False, fatal=True, exc_list=None):
87+ """
88+ Return the assigned IPv6 address for a given interface, if any, or [].
89+ """
90+ addresses = get_iface_addr(iface=iface, inet_type='AF_INET6',
91+ inc_aliases=inc_aliases, fatal=fatal,
92+ exc_list=exc_list)
93+ remotly_addressable = []
94+ for address in addresses:
95+ if not address.startswith('fe80'):
96+ remotly_addressable.append(address)
97+ if fatal and not remotly_addressable:
98+ raise Exception("Interface '%s' doesn't have global ipv6 address." % iface)
99+ return remotly_addressable
100+
101+
102+def get_bridges(vnic_dir='/sys/devices/virtual/net'):
103+ """
104+ Return a list of bridges on the system or []
105+ """
106+ b_rgex = vnic_dir + '/*/bridge'
107+ return [x.replace(vnic_dir, '').split('/')[1] for x in glob.glob(b_rgex)]
108+
109+
110+def get_bridge_nics(bridge, vnic_dir='/sys/devices/virtual/net'):
111+ """
112+ Return a list of nics comprising a given bridge on the system or []
113+ """
114+ brif_rgex = "%s/%s/brif/*" % (vnic_dir, bridge)
115+ return [x.split('/')[-1] for x in glob.glob(brif_rgex)]
116+
117+
118+def is_bridge_member(nic):
119+ """
120+ Check if a given nic is a member of a bridge
121+ """
122+ for bridge in get_bridges():
123+ if nic in get_bridge_nics(bridge):
124+ return True
125+ return False
126
127=== modified file 'hooks/charmhelpers/contrib/storage/linux/utils.py'
128--- hooks/charmhelpers/contrib/storage/linux/utils.py 2014-07-25 08:07:41 +0000
129+++ hooks/charmhelpers/contrib/storage/linux/utils.py 2014-09-21 19:34:01 +0000
130@@ -46,5 +46,8 @@
131 :returns: boolean: True if the path represents a mounted device, False if
132 it doesn't.
133 '''
134+ is_partition = bool(re.search(r".*[0-9]+\b", device))
135 out = check_output(['mount'])
136+ if is_partition:
137+ return bool(re.search(device + r"\b", out))
138 return bool(re.search(device + r"[0-9]+\b", out))
139
140=== modified file 'hooks/charmhelpers/core/hookenv.py'
141--- hooks/charmhelpers/core/hookenv.py 2014-07-25 08:07:41 +0000
142+++ hooks/charmhelpers/core/hookenv.py 2014-09-21 19:34:01 +0000
143@@ -156,12 +156,15 @@
144
145
146 class Config(dict):
147- """A Juju charm config dictionary that can write itself to
148- disk (as json) and track which values have changed since
149- the previous hook invocation.
150-
151- Do not instantiate this object directly - instead call
152- ``hookenv.config()``
153+ """A dictionary representation of the charm's config.yaml, with some
154+ extra features:
155+
156+ - See which values in the dictionary have changed since the previous hook.
157+ - For values that have changed, see what the previous value was.
158+ - Store arbitrary data for use in a later hook.
159+
160+ NOTE: Do not instantiate this object directly - instead call
161+ ``hookenv.config()``, which will return an instance of :class:`Config`.
162
163 Example usage::
164
165@@ -170,8 +173,8 @@
166 >>> config = hookenv.config()
167 >>> config['foo']
168 'bar'
169+ >>> # store a new key/value for later use
170 >>> config['mykey'] = 'myval'
171- >>> config.save()
172
173
174 >>> # user runs `juju set mycharm foo=baz`
175@@ -188,22 +191,34 @@
176 >>> # keys/values that we add are preserved across hooks
177 >>> config['mykey']
178 'myval'
179- >>> # don't forget to save at the end of hook!
180- >>> config.save()
181
182 """
183 CONFIG_FILE_NAME = '.juju-persistent-config'
184
185 def __init__(self, *args, **kw):
186 super(Config, self).__init__(*args, **kw)
187+ self.implicit_save = True
188 self._prev_dict = None
189 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
190 if os.path.exists(self.path):
191 self.load_previous()
192
193+ def __getitem__(self, key):
194+ """For regular dict lookups, check the current juju config first,
195+ then the previous (saved) copy. This ensures that user-saved values
196+ will be returned by a dict lookup.
197+
198+ """
199+ try:
200+ return dict.__getitem__(self, key)
201+ except KeyError:
202+ return (self._prev_dict or {})[key]
203+
204 def load_previous(self, path=None):
205- """Load previous copy of config from disk so that current values
206- can be compared to previous values.
207+ """Load previous copy of config from disk.
208+
209+ In normal usage you don't need to call this method directly - it
210+ is called automatically at object initialization.
211
212 :param path:
213
214@@ -218,8 +233,8 @@
215 self._prev_dict = json.load(f)
216
217 def changed(self, key):
218- """Return true if the value for this key has changed since
219- the last save.
220+ """Return True if the current value for this key is different from
221+ the previous value.
222
223 """
224 if self._prev_dict is None:
225@@ -228,7 +243,7 @@
226
227 def previous(self, key):
228 """Return previous value for this key, or None if there
229- is no "previous" value.
230+ is no previous value.
231
232 """
233 if self._prev_dict:
234@@ -238,7 +253,13 @@
235 def save(self):
236 """Save this config to disk.
237
238- Preserves items in _prev_dict that do not exist in self.
239+ If the charm is using the :mod:`Services Framework <services.base>`
240+ or :meth:'@hook <Hooks.hook>' decorator, this
241+ is called automatically at the end of successful hook execution.
242+ Otherwise, it should be called directly by user code.
243+
244+ To disable automatic saves, set ``implicit_save=False`` on this
245+ instance.
246
247 """
248 if self._prev_dict:
249@@ -285,8 +306,9 @@
250 raise
251
252
253-def relation_set(relation_id=None, relation_settings={}, **kwargs):
254+def relation_set(relation_id=None, relation_settings=None, **kwargs):
255 """Set relation information for the current unit"""
256+ relation_settings = relation_settings if relation_settings else {}
257 relation_cmd_line = ['relation-set']
258 if relation_id is not None:
259 relation_cmd_line.extend(('-r', relation_id))
260@@ -477,6 +499,9 @@
261 hook_name = os.path.basename(args[0])
262 if hook_name in self._hooks:
263 self._hooks[hook_name]()
264+ cfg = config()
265+ if cfg.implicit_save:
266+ cfg.save()
267 else:
268 raise UnregisteredHookError(hook_name)
269
270
271=== modified file 'hooks/charmhelpers/core/host.py'
272--- hooks/charmhelpers/core/host.py 2014-07-25 08:07:41 +0000
273+++ hooks/charmhelpers/core/host.py 2014-09-21 19:34:01 +0000
274@@ -12,6 +12,8 @@
275 import string
276 import subprocess
277 import hashlib
278+import shutil
279+from contextlib import contextmanager
280
281 from collections import OrderedDict
282
283@@ -52,7 +54,7 @@
284 def service_running(service):
285 """Determine whether a system service is running"""
286 try:
287- output = subprocess.check_output(['service', service, 'status'])
288+ output = subprocess.check_output(['service', service, 'status'], stderr=subprocess.STDOUT)
289 except subprocess.CalledProcessError:
290 return False
291 else:
292@@ -62,6 +64,16 @@
293 return False
294
295
296+def service_available(service_name):
297+ """Determine whether a system service is available"""
298+ try:
299+ subprocess.check_output(['service', service_name, 'status'], stderr=subprocess.STDOUT)
300+ except subprocess.CalledProcessError:
301+ return False
302+ else:
303+ return True
304+
305+
306 def adduser(username, password=None, shell='/bin/bash', system_user=False):
307 """Add a user to the system"""
308 try:
309@@ -320,12 +332,29 @@
310
311 '''
312 import apt_pkg
313+ from charmhelpers.fetch import apt_cache
314 if not pkgcache:
315- apt_pkg.init()
316- # Force Apt to build its cache in memory. That way we avoid race
317- # conditions with other applications building the cache in the same
318- # place.
319- apt_pkg.config.set("Dir::Cache::pkgcache", "")
320- pkgcache = apt_pkg.Cache()
321+ pkgcache = apt_cache()
322 pkg = pkgcache[package]
323 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
324+
325+
326+@contextmanager
327+def chdir(d):
328+ cur = os.getcwd()
329+ try:
330+ yield os.chdir(d)
331+ finally:
332+ os.chdir(cur)
333+
334+
335+def chownr(path, owner, group):
336+ uid = pwd.getpwnam(owner).pw_uid
337+ gid = grp.getgrnam(group).gr_gid
338+
339+ for root, dirs, files in os.walk(path):
340+ for name in dirs + files:
341+ full = os.path.join(root, name)
342+ broken_symlink = os.path.lexists(full) and not os.path.exists(full)
343+ if not broken_symlink:
344+ os.chown(full, uid, gid)
345
346=== added directory 'hooks/charmhelpers/core/services'
347=== added file 'hooks/charmhelpers/core/services/__init__.py'
348--- hooks/charmhelpers/core/services/__init__.py 1970-01-01 00:00:00 +0000
349+++ hooks/charmhelpers/core/services/__init__.py 2014-09-21 19:34:01 +0000
350@@ -0,0 +1,2 @@
351+from .base import *
352+from .helpers import *
353
354=== added file 'hooks/charmhelpers/core/services/base.py'
355--- hooks/charmhelpers/core/services/base.py 1970-01-01 00:00:00 +0000
356+++ hooks/charmhelpers/core/services/base.py 2014-09-21 19:34:01 +0000
357@@ -0,0 +1,313 @@
358+import os
359+import re
360+import json
361+from collections import Iterable
362+
363+from charmhelpers.core import host
364+from charmhelpers.core import hookenv
365+
366+
367+__all__ = ['ServiceManager', 'ManagerCallback',
368+ 'PortManagerCallback', 'open_ports', 'close_ports', 'manage_ports',
369+ 'service_restart', 'service_stop']
370+
371+
372+class ServiceManager(object):
373+ def __init__(self, services=None):
374+ """
375+ Register a list of services, given their definitions.
376+
377+ Service definitions are dicts in the following formats (all keys except
378+ 'service' are optional)::
379+
380+ {
381+ "service": <service name>,
382+ "required_data": <list of required data contexts>,
383+ "provided_data": <list of provided data contexts>,
384+ "data_ready": <one or more callbacks>,
385+ "data_lost": <one or more callbacks>,
386+ "start": <one or more callbacks>,
387+ "stop": <one or more callbacks>,
388+ "ports": <list of ports to manage>,
389+ }
390+
391+ The 'required_data' list should contain dicts of required data (or
392+ dependency managers that act like dicts and know how to collect the data).
393+ Only when all items in the 'required_data' list are populated are the list
394+ of 'data_ready' and 'start' callbacks executed. See `is_ready()` for more
395+ information.
396+
397+ The 'provided_data' list should contain relation data providers, most likely
398+ a subclass of :class:`charmhelpers.core.services.helpers.RelationContext`,
399+ that will indicate a set of data to set on a given relation.
400+
401+ The 'data_ready' value should be either a single callback, or a list of
402+ callbacks, to be called when all items in 'required_data' pass `is_ready()`.
403+ Each callback will be called with the service name as the only parameter.
404+ After all of the 'data_ready' callbacks are called, the 'start' callbacks
405+ are fired.
406+
407+ The 'data_lost' value should be either a single callback, or a list of
408+ callbacks, to be called when a 'required_data' item no longer passes
409+ `is_ready()`. Each callback will be called with the service name as the
410+ only parameter. After all of the 'data_lost' callbacks are called,
411+ the 'stop' callbacks are fired.
412+
413+ The 'start' value should be either a single callback, or a list of
414+ callbacks, to be called when starting the service, after the 'data_ready'
415+ callbacks are complete. Each callback will be called with the service
416+ name as the only parameter. This defaults to
417+ `[host.service_start, services.open_ports]`.
418+
419+ The 'stop' value should be either a single callback, or a list of
420+ callbacks, to be called when stopping the service. If the service is
421+ being stopped because it no longer has all of its 'required_data', this
422+ will be called after all of the 'data_lost' callbacks are complete.
423+ Each callback will be called with the service name as the only parameter.
424+ This defaults to `[services.close_ports, host.service_stop]`.
425+
426+ The 'ports' value should be a list of ports to manage. The default
427+ 'start' handler will open the ports after the service is started,
428+ and the default 'stop' handler will close the ports prior to stopping
429+ the service.
430+
431+
432+ Examples:
433+
434+ The following registers an Upstart service called bingod that depends on
435+ a mongodb relation and which runs a custom `db_migrate` function prior to
436+ restarting the service, and a Runit service called spadesd::
437+
438+ manager = services.ServiceManager([
439+ {
440+ 'service': 'bingod',
441+ 'ports': [80, 443],
442+ 'required_data': [MongoRelation(), config(), {'my': 'data'}],
443+ 'data_ready': [
444+ services.template(source='bingod.conf'),
445+ services.template(source='bingod.ini',
446+ target='/etc/bingod.ini',
447+ owner='bingo', perms=0400),
448+ ],
449+ },
450+ {
451+ 'service': 'spadesd',
452+ 'data_ready': services.template(source='spadesd_run.j2',
453+ target='/etc/sv/spadesd/run',
454+ perms=0555),
455+ 'start': runit_start,
456+ 'stop': runit_stop,
457+ },
458+ ])
459+ manager.manage()
460+ """
461+ self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
462+ self._ready = None
463+ self.services = {}
464+ for service in services or []:
465+ service_name = service['service']
466+ self.services[service_name] = service
467+
468+ def manage(self):
469+ """
470+ Handle the current hook by doing The Right Thing with the registered services.
471+ """
472+ hook_name = hookenv.hook_name()
473+ if hook_name == 'stop':
474+ self.stop_services()
475+ else:
476+ self.provide_data()
477+ self.reconfigure_services()
478+ cfg = hookenv.config()
479+ if cfg.implicit_save:
480+ cfg.save()
481+
482+ def provide_data(self):
483+ """
484+ Set the relation data for each provider in the ``provided_data`` list.
485+
486+ A provider must have a `name` attribute, which indicates which relation
487+ to set data on, and a `provide_data()` method, which returns a dict of
488+ data to set.
489+ """
490+ hook_name = hookenv.hook_name()
491+ for service in self.services.values():
492+ for provider in service.get('provided_data', []):
493+ if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
494+ data = provider.provide_data()
495+ _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data
496+ if _ready:
497+ hookenv.relation_set(None, data)
498+
499+ def reconfigure_services(self, *service_names):
500+ """
501+ Update all files for one or more registered services, and,
502+ if ready, optionally restart them.
503+
504+ If no service names are given, reconfigures all registered services.
505+ """
506+ for service_name in service_names or self.services.keys():
507+ if self.is_ready(service_name):
508+ self.fire_event('data_ready', service_name)
509+ self.fire_event('start', service_name, default=[
510+ service_restart,
511+ manage_ports])
512+ self.save_ready(service_name)
513+ else:
514+ if self.was_ready(service_name):
515+ self.fire_event('data_lost', service_name)
516+ self.fire_event('stop', service_name, default=[
517+ manage_ports,
518+ service_stop])
519+ self.save_lost(service_name)
520+
521+ def stop_services(self, *service_names):
522+ """
523+ Stop one or more registered services, by name.
524+
525+ If no service names are given, stops all registered services.
526+ """
527+ for service_name in service_names or self.services.keys():
528+ self.fire_event('stop', service_name, default=[
529+ manage_ports,
530+ service_stop])
531+
532+ def get_service(self, service_name):
533+ """
534+ Given the name of a registered service, return its service definition.
535+ """
536+ service = self.services.get(service_name)
537+ if not service:
538+ raise KeyError('Service not registered: %s' % service_name)
539+ return service
540+
541+ def fire_event(self, event_name, service_name, default=None):
542+ """
543+ Fire a data_ready, data_lost, start, or stop event on a given service.
544+ """
545+ service = self.get_service(service_name)
546+ callbacks = service.get(event_name, default)
547+ if not callbacks:
548+ return
549+ if not isinstance(callbacks, Iterable):
550+ callbacks = [callbacks]
551+ for callback in callbacks:
552+ if isinstance(callback, ManagerCallback):
553+ callback(self, service_name, event_name)
554+ else:
555+ callback(service_name)
556+
557+ def is_ready(self, service_name):
558+ """
559+ Determine if a registered service is ready, by checking its 'required_data'.
560+
561+ A 'required_data' item can be any mapping type, and is considered ready
562+ if `bool(item)` evaluates as True.
563+ """
564+ service = self.get_service(service_name)
565+ reqs = service.get('required_data', [])
566+ return all(bool(req) for req in reqs)
567+
568+ def _load_ready_file(self):
569+ if self._ready is not None:
570+ return
571+ if os.path.exists(self._ready_file):
572+ with open(self._ready_file) as fp:
573+ self._ready = set(json.load(fp))
574+ else:
575+ self._ready = set()
576+
577+ def _save_ready_file(self):
578+ if self._ready is None:
579+ return
580+ with open(self._ready_file, 'w') as fp:
581+ json.dump(list(self._ready), fp)
582+
583+ def save_ready(self, service_name):
584+ """
585+ Save an indicator that the given service is now data_ready.
586+ """
587+ self._load_ready_file()
588+ self._ready.add(service_name)
589+ self._save_ready_file()
590+
591+ def save_lost(self, service_name):
592+ """
593+ Save an indicator that the given service is no longer data_ready.
594+ """
595+ self._load_ready_file()
596+ self._ready.discard(service_name)
597+ self._save_ready_file()
598+
599+ def was_ready(self, service_name):
600+ """
601+ Determine if the given service was previously data_ready.
602+ """
603+ self._load_ready_file()
604+ return service_name in self._ready
605+
606+
607+class ManagerCallback(object):
608+ """
609+ Special case of a callback that takes the `ServiceManager` instance
610+ in addition to the service name.
611+
612+ Subclasses should implement `__call__` which should accept three parameters:
613+
614+ * `manager` The `ServiceManager` instance
615+ * `service_name` The name of the service it's being triggered for
616+ * `event_name` The name of the event that this callback is handling
617+ """
618+ def __call__(self, manager, service_name, event_name):
619+ raise NotImplementedError()
620+
621+
622+class PortManagerCallback(ManagerCallback):
623+ """
624+ Callback class that will open or close ports, for use as either
625+ a start or stop action.
626+ """
627+ def __call__(self, manager, service_name, event_name):
628+ service = manager.get_service(service_name)
629+ new_ports = service.get('ports', [])
630+ port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name))
631+ if os.path.exists(port_file):
632+ with open(port_file) as fp:
633+ old_ports = fp.read().split(',')
634+ for old_port in old_ports:
635+ if bool(old_port):
636+ old_port = int(old_port)
637+ if old_port not in new_ports:
638+ hookenv.close_port(old_port)
639+ with open(port_file, 'w') as fp:
640+ fp.write(','.join(str(port) for port in new_ports))
641+ for port in new_ports:
642+ if event_name == 'start':
643+ hookenv.open_port(port)
644+ elif event_name == 'stop':
645+ hookenv.close_port(port)
646+
647+
648+def service_stop(service_name):
649+ """
650+ Wrapper around host.service_stop to prevent spurious "unknown service"
651+ messages in the logs.
652+ """
653+ if host.service_running(service_name):
654+ host.service_stop(service_name)
655+
656+
657+def service_restart(service_name):
658+ """
659+ Wrapper around host.service_restart to prevent spurious "unknown service"
660+ messages in the logs.
661+ """
662+ if host.service_available(service_name):
663+ if host.service_running(service_name):
664+ host.service_restart(service_name)
665+ else:
666+ host.service_start(service_name)
667+
668+
669+# Convenience aliases
670+open_ports = close_ports = manage_ports = PortManagerCallback()
671
672=== added file 'hooks/charmhelpers/core/services/helpers.py'
673--- hooks/charmhelpers/core/services/helpers.py 1970-01-01 00:00:00 +0000
674+++ hooks/charmhelpers/core/services/helpers.py 2014-09-21 19:34:01 +0000
675@@ -0,0 +1,125 @@
676+from charmhelpers.core import hookenv
677+from charmhelpers.core import templating
678+
679+from charmhelpers.core.services.base import ManagerCallback
680+
681+
682+__all__ = ['RelationContext', 'TemplateCallback',
683+ 'render_template', 'template']
684+
685+
686+class RelationContext(dict):
687+ """
688+ Base class for a context generator that gets relation data from juju.
689+
690+ Subclasses must provide the attributes `name`, which is the name of the
691+ interface of interest, `interface`, which is the type of the interface of
692+ interest, and `required_keys`, which is the set of keys required for the
693+ relation to be considered complete. The data for all interfaces matching
694+ the `name` attribute that are complete will used to populate the dictionary
695+ values (see `get_data`, below).
696+
697+ The generated context will be namespaced under the interface type, to prevent
698+ potential naming conflicts.
699+ """
700+ name = None
701+ interface = None
702+ required_keys = []
703+
704+ def __init__(self, *args, **kwargs):
705+ super(RelationContext, self).__init__(*args, **kwargs)
706+ self.get_data()
707+
708+ def __bool__(self):
709+ """
710+ Returns True if all of the required_keys are available.
711+ """
712+ return self.is_ready()
713+
714+ __nonzero__ = __bool__
715+
716+ def __repr__(self):
717+ return super(RelationContext, self).__repr__()
718+
719+ def is_ready(self):
720+ """
721+ Returns True if all of the `required_keys` are available from any units.
722+ """
723+ ready = len(self.get(self.name, [])) > 0
724+ if not ready:
725+ hookenv.log('Incomplete relation: {}'.format(self.__class__.__name__), hookenv.DEBUG)
726+ return ready
727+
728+ def _is_ready(self, unit_data):
729+ """
730+ Helper method that tests a set of relation data and returns True if
731+ all of the `required_keys` are present.
732+ """
733+ return set(unit_data.keys()).issuperset(set(self.required_keys))
734+
735+ def get_data(self):
736+ """
737+ Retrieve the relation data for each unit involved in a relation and,
738+ if complete, store it in a list under `self[self.name]`. This
739+ is automatically called when the RelationContext is instantiated.
740+
741+ The units are sorted lexographically first by the service ID, then by
742+ the unit ID. Thus, if an interface has two other services, 'db:1'
743+ and 'db:2', with 'db:1' having two units, 'wordpress/0' and 'wordpress/1',
744+ and 'db:2' having one unit, 'mediawiki/0', all of which have a complete
745+ set of data, the relation data for the units will be stored in the
746+ order: 'wordpress/0', 'wordpress/1', 'mediawiki/0'.
747+
748+ If you only care about a single unit on the relation, you can just
749+ access it as `{{ interface[0]['key'] }}`. However, if you can at all
750+ support multiple units on a relation, you should iterate over the list,
751+ like::
752+
753+ {% for unit in interface -%}
754+ {{ unit['key'] }}{% if not loop.last %},{% endif %}
755+ {%- endfor %}
756+
757+ Note that since all sets of relation data from all related services and
758+ units are in a single list, if you need to know which service or unit a
759+ set of data came from, you'll need to extend this class to preserve
760+ that information.
761+ """
762+ if not hookenv.relation_ids(self.name):
763+ return
764+
765+ ns = self.setdefault(self.name, [])
766+ for rid in sorted(hookenv.relation_ids(self.name)):
767+ for unit in sorted(hookenv.related_units(rid)):
768+ reldata = hookenv.relation_get(rid=rid, unit=unit)
769+ if self._is_ready(reldata):
770+ ns.append(reldata)
771+
772+ def provide_data(self):
773+ """
774+ Return data to be relation_set for this interface.
775+ """
776+ return {}
777+
778+
779+class TemplateCallback(ManagerCallback):
780+ """
781+ Callback class that will render a template, for use as a ready action.
782+ """
783+ def __init__(self, source, target, owner='root', group='root', perms=0444):
784+ self.source = source
785+ self.target = target
786+ self.owner = owner
787+ self.group = group
788+ self.perms = perms
789+
790+ def __call__(self, manager, service_name, event_name):
791+ service = manager.get_service(service_name)
792+ context = {}
793+ for ctx in service.get('required_data', []):
794+ context.update(ctx)
795+ templating.render(self.source, self.target, context,
796+ self.owner, self.group, self.perms)
797+
798+
799+# Convenience aliases for templates
800+render_template = template = TemplateCallback
801
802=== added file 'hooks/charmhelpers/core/templating.py'
803--- hooks/charmhelpers/core/templating.py 1970-01-01 00:00:00 +0000
804+++ hooks/charmhelpers/core/templating.py 2014-09-21 19:34:01 +0000
805@@ -0,0 +1,51 @@
806+import os
807+
808+from charmhelpers.core import host
809+from charmhelpers.core import hookenv
810+
811+
812+def render(source, target, context, owner='root', group='root', perms=0444, templates_dir=None):
813+ """
814+ Render a template.
815+
816+ The `source` path, if not absolute, is relative to the `templates_dir`.
817+
818+ The `target` path should be absolute.
819+
820+ The context should be a dict containing the values to be replaced in the
821+ template.
822+
823+ The `owner`, `group`, and `perms` options will be passed to `write_file`.
824+
825+ If omitted, `templates_dir` defaults to the `templates` folder in the charm.
826+
827+ Note: Using this requires python-jinja2; if it is not installed, calling
828+ this will attempt to use charmhelpers.fetch.apt_install to install it.
829+ """
830+ try:
831+ from jinja2 import FileSystemLoader, Environment, exceptions
832+ except ImportError:
833+ try:
834+ from charmhelpers.fetch import apt_install
835+ except ImportError:
836+ hookenv.log('Could not import jinja2, and could not import '
837+ 'charmhelpers.fetch to install it',
838+ level=hookenv.ERROR)
839+ raise
840+ apt_install('python-jinja2', fatal=True)
841+ from jinja2 import FileSystemLoader, Environment, exceptions
842+
843+ if templates_dir is None:
844+ templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
845+ loader = Environment(loader=FileSystemLoader(templates_dir))
846+ try:
847+ source = source
848+ template = loader.get_template(source)
849+ except exceptions.TemplateNotFound as e:
850+ hookenv.log('Could not load template %s from %s.' %
851+ (source, templates_dir),
852+ level=hookenv.ERROR)
853+ raise e
854+ content = template.render(context)
855+ host.mkdir(os.path.dirname(target))
856+ host.write_file(target, content, owner, group, perms)
857
858=== modified file 'hooks/charmhelpers/fetch/__init__.py'
859--- hooks/charmhelpers/fetch/__init__.py 2014-07-25 08:07:41 +0000
860+++ hooks/charmhelpers/fetch/__init__.py 2014-09-21 19:34:01 +0000
861@@ -1,4 +1,5 @@
862 import importlib
863+from tempfile import NamedTemporaryFile
864 import time
865 from yaml import safe_load
866 from charmhelpers.core.host import (
867@@ -116,14 +117,7 @@
868
869 def filter_installed_packages(packages):
870 """Returns a list of packages that require installation"""
871- import apt_pkg
872- apt_pkg.init()
873-
874- # Tell apt to build an in-memory cache to prevent race conditions (if
875- # another process is already building the cache).
876- apt_pkg.config.set("Dir::Cache::pkgcache", "")
877-
878- cache = apt_pkg.Cache()
879+ cache = apt_cache()
880 _pkgs = []
881 for package in packages:
882 try:
883@@ -136,6 +130,16 @@
884 return _pkgs
885
886
887+def apt_cache(in_memory=True):
888+ """Build and return an apt cache"""
889+ import apt_pkg
890+ apt_pkg.init()
891+ if in_memory:
892+ apt_pkg.config.set("Dir::Cache::pkgcache", "")
893+ apt_pkg.config.set("Dir::Cache::srcpkgcache", "")
894+ return apt_pkg.Cache()
895+
896+
897 def apt_install(packages, options=None, fatal=False):
898 """Install one or more packages"""
899 if options is None:
900@@ -201,6 +205,27 @@
901
902
903 def add_source(source, key=None):
904+ """Add a package source to this system.
905+
906+ @param source: a URL or sources.list entry, as supported by
907+ add-apt-repository(1). Examples:
908+ ppa:charmers/example
909+ deb https://stub:key@private.example.com/ubuntu trusty main
910+
911+ In addition:
912+ 'proposed:' may be used to enable the standard 'proposed'
913+ pocket for the release.
914+ 'cloud:' may be used to activate official cloud archive pockets,
915+ such as 'cloud:icehouse'
916+
917+ @param key: A key to be added to the system's APT keyring and used
918+ to verify the signatures on packages. Ideally, this should be an
919+ ASCII format GPG public key including the block headers. A GPG key
920+ id may also be used, but be aware that only insecure protocols are
921+ available to retrieve the actual public key from a public keyserver
922+ placing your Juju environment at risk. ppa and cloud archive keys
923+ are securely added automtically, so sould not be provided.
924+ """
925 if source is None:
926 log('Source is not present. Skipping')
927 return
928@@ -225,10 +250,23 @@
929 release = lsb_release()['DISTRIB_CODENAME']
930 with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
931 apt.write(PROPOSED_POCKET.format(release))
932+ else:
933+ raise SourceConfigError("Unknown source: {!r}".format(source))
934+
935 if key:
936- subprocess.check_call(['apt-key', 'adv', '--keyserver',
937- 'hkp://keyserver.ubuntu.com:80', '--recv',
938- key])
939+ if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
940+ with NamedTemporaryFile() as key_file:
941+ key_file.write(key)
942+ key_file.flush()
943+ key_file.seek(0)
944+ subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)
945+ else:
946+ # Note that hkp: is in no way a secure protocol. Using a
947+ # GPG key id is pointless from a security POV unless you
948+ # absolutely trust your network and DNS.
949+ subprocess.check_call(['apt-key', 'adv', '--keyserver',
950+ 'hkp://keyserver.ubuntu.com:80', '--recv',
951+ key])
952
953
954 def configure_sources(update=False,
955@@ -238,7 +276,8 @@
956 Configure multiple sources from charm configuration.
957
958 The lists are encoded as yaml fragments in the configuration.
959- The frament needs to be included as a string.
960+ The frament needs to be included as a string. Sources and their
961+ corresponding keys are of the types supported by add_source().
962
963 Example config:
964 install_sources: |
965
966=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
967--- hooks/charmhelpers/fetch/archiveurl.py 2014-04-16 08:37:56 +0000
968+++ hooks/charmhelpers/fetch/archiveurl.py 2014-09-21 19:34:01 +0000
969@@ -1,6 +1,8 @@
970 import os
971 import urllib2
972+from urllib import urlretrieve
973 import urlparse
974+import hashlib
975
976 from charmhelpers.fetch import (
977 BaseFetchHandler,
978@@ -12,7 +14,17 @@
979 )
980 from charmhelpers.core.host import mkdir
981
982-
983+"""
984+This class is a plugin for charmhelpers.fetch.install_remote.
985+
986+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/.
987+
988+Example usage:
989+install_remote("https://example.com/some/archive.tar.gz")
990+# Installs the contents of archive.tar.gz in $CHARM_DIR/fetched/.
991+
992+See charmhelpers.fetch.archiveurl.get_archivehandler for supported archive types.
993+"""
994 class ArchiveUrlFetchHandler(BaseFetchHandler):
995 """Handler for archives via generic URLs"""
996 def can_handle(self, source):
997@@ -61,3 +73,31 @@
998 except OSError as e:
999 raise UnhandledSource(e.strerror)
1000 return extract(dld_file)
1001+
1002+ # Mandatory file validation via Sha1 or MD5 hashing.
1003+ def download_and_validate(self, url, hashsum, validate="sha1"):
1004+ if validate == 'sha1' and len(hashsum) != 40:
1005+ raise ValueError("HashSum must be = 40 characters when using sha1"
1006+ " validation")
1007+ if validate == 'md5' and len(hashsum) != 32:
1008+ raise ValueError("HashSum must be = 32 characters when using md5"
1009+ " validation")
1010+ tempfile, headers = urlretrieve(url)
1011+ self.validate_file(tempfile, hashsum, validate)
1012+ return tempfile
1013+
1014+ # Predicate method that returns status of hash matching expected hash.
1015+ def validate_file(self, source, hashsum, vmethod='sha1'):
1016+ if vmethod != 'sha1' and vmethod != 'md5':
1017+ raise ValueError("Validation Method not supported")
1018+
1019+ if vmethod == 'md5':
1020+ m = hashlib.md5()
1021+ if vmethod == 'sha1':
1022+ m = hashlib.sha1()
1023+ with open(source) as f:
1024+ for line in f:
1025+ m.update(line)
1026+ if hashsum != m.hexdigest():
1027+ msg = "Hash Mismatch on {} expected {} got {}"
1028+ raise ValueError(msg.format(source, hashsum, m.hexdigest()))
1029
1030=== modified file 'hooks/hooks.py'
1031--- hooks/hooks.py 2014-08-15 09:06:49 +0000
1032+++ hooks/hooks.py 2014-09-21 19:34:01 +0000
1033@@ -15,14 +15,17 @@
1034 import ceph
1035 from charmhelpers.core.hookenv import (
1036 log,
1037+ WARNING,
1038 ERROR,
1039 config,
1040 relation_ids,
1041 related_units,
1042 relation_get,
1043+ relation_set,
1044 Hooks,
1045 UnregisteredHookError,
1046- service_name
1047+ service_name,
1048+ unit_get
1049 )
1050 from charmhelpers.core.host import (
1051 umount,
1052@@ -39,10 +42,14 @@
1053 from utils import (
1054 render_template,
1055 get_host_ip,
1056+ setup_ipv6
1057 )
1058
1059 from charmhelpers.contrib.openstack.alternatives import install_alternative
1060-from charmhelpers.contrib.network.ip import is_ipv6
1061+from charmhelpers.contrib.network.ip import (
1062+ is_ipv6,
1063+ get_ipv6_addr
1064+)
1065
1066 hooks = Hooks()
1067
1068@@ -58,6 +65,10 @@
1069 def install():
1070 add_source(config('source'), config('key'))
1071 apt_update(fatal=True)
1072+
1073+ if config('prefer-ipv6'):
1074+ setup_ipv6()
1075+
1076 apt_install(packages=ceph.PACKAGES, fatal=True)
1077 install_upstart_scripts()
1078
1079@@ -76,6 +87,14 @@
1080 'ceph_public_network': config('ceph-public-network'),
1081 'ceph_cluster_network': config('ceph-cluster-network'),
1082 }
1083+
1084+ if config('prefer-ipv6'):
1085+ host_ip = get_ipv6_addr()[0]
1086+ if host_ip:
1087+ cephcontext['host_ip'] = host_ip
1088+ else:
1089+ log("Unable to obtain host address", level=WARNING)
1090+
1091 # Install ceph.conf as an alternative to support
1092 # co-existence with other charms that write this file
1093 charm_ceph_conf = "/var/lib/charm/{}/ceph.conf".format(service_name())
1094@@ -95,6 +114,9 @@
1095 log('Invalid OSD disk format configuration specified', level=ERROR)
1096 sys.exit(1)
1097
1098+ if config('prefer-ipv6'):
1099+ setup_ipv6()
1100+
1101 e_mountpoint = config('ephemeral-unmount')
1102 if (e_mountpoint and ceph.filesystem_mounted(e_mountpoint)):
1103 umount(e_mountpoint)
1104@@ -120,8 +142,13 @@
1105 hosts = []
1106 for relid in relation_ids('mon'):
1107 for unit in related_units(relid):
1108- addr = relation_get('ceph-public-address', unit, relid) or \
1109- get_host_ip(relation_get('private-address', unit, relid))
1110+ addr = relation_get('ceph-public-address', unit, relid)
1111+ if not addr:
1112+ if config('prefer-ipv6'):
1113+ addr = relation_get('private-address', unit, relid)
1114+ else:
1115+ get_host_ip(relation_get('private-address', unit, relid))
1116+
1117 if addr is not None:
1118 if is_ipv6(addr):
1119 hosts.append('[{}]:6789'.format(addr))
1120@@ -166,6 +193,17 @@
1121 @hooks.hook('mon-relation-changed',
1122 'mon-relation-departed')
1123 def mon_relation():
1124+ if config('prefer-ipv6'):
1125+ host = get_ipv6_addr()[0]
1126+ else:
1127+ host = unit_get('private-address')
1128+
1129+ if host:
1130+ relation_data = {'private-address': host}
1131+ relation_set(**relation_data)
1132+ else:
1133+ log("Unable to obtain host address", level=WARNING)
1134+
1135 bootstrap_key = relation_get('osd_bootstrap_key')
1136 if get_fsid() and get_auth() and bootstrap_key:
1137 log('mon has provided conf- scanning disks')
1138
1139=== modified file 'hooks/utils.py'
1140--- hooks/utils.py 2013-10-10 10:49:36 +0000
1141+++ hooks/utils.py 2014-09-21 19:34:01 +0000
1142@@ -18,6 +18,10 @@
1143 filter_installed_packages
1144 )
1145
1146+from charmhelpers.core.host import (
1147+ lsb_release
1148+)
1149+
1150 TEMPLATES_DIR = 'templates'
1151
1152 try:
1153@@ -72,3 +76,10 @@
1154 answers = dns.resolver.query(hostname, 'A')
1155 if answers:
1156 return answers[0].address
1157+
1158+
1159+def setup_ipv6():
1160+ ubuntu_rel = float(lsb_release()['DISTRIB_RELEASE'])
1161+ if ubuntu_rel < 14.04:
1162+ raise Exception("IPv6 is not supported for Ubuntu "
1163+ "versions less than Trusty 14.04")
1164
1165=== modified file 'templates/ceph.conf'
1166--- templates/ceph.conf 2014-07-25 08:07:41 +0000
1167+++ templates/ceph.conf 2014-09-21 19:34:01 +0000
1168@@ -32,3 +32,6 @@
1169 osd journal size = {{ osd_journal_size }}
1170 filestore xattr use omap = true
1171
1172+ host = {{ hostname }}
1173+ public addr = {{ host_ip }}
1174+ cluster addr = {{ host_ip }}
1175\ No newline at end of file

Subscribers

People subscribed via source and target branches