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
=== modified file 'config.yaml'
--- config.yaml 2014-08-15 09:06:49 +0000
+++ config.yaml 2014-09-21 19:34:01 +0000
@@ -103,3 +103,6 @@
103 description: |103 description: |
104 The IP address and netmask of the cluster (back-side) network (e.g.,104 The IP address and netmask of the cluster (back-side) network (e.g.,
105 192.168.0.0/24)105 192.168.0.0/24)
106 prefer-ipv6:
107 type: boolean
108 default: False
106\ No newline at end of file109\ No newline at end of file
107110
=== modified file 'hooks/charmhelpers/contrib/network/ip.py'
--- hooks/charmhelpers/contrib/network/ip.py 2014-07-25 08:07:41 +0000
+++ hooks/charmhelpers/contrib/network/ip.py 2014-09-21 19:34:01 +0000
@@ -1,3 +1,4 @@
1import glob
1import sys2import sys
23
3from functools import partial4from functools import partial
@@ -154,3 +155,104 @@
154get_iface_for_address = partial(_get_for_address, key='iface')155get_iface_for_address = partial(_get_for_address, key='iface')
155156
156get_netmask_for_address = partial(_get_for_address, key='netmask')157get_netmask_for_address = partial(_get_for_address, key='netmask')
158
159
160def format_ipv6_addr(address):
161 """
162 IPv6 needs to be wrapped with [] in url link to parse correctly.
163 """
164 if is_ipv6(address):
165 address = "[%s]" % address
166 else:
167 log("Not an valid ipv6 address: %s" % address,
168 level=ERROR)
169 address = None
170 return address
171
172
173def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False, fatal=True, exc_list=None):
174 """
175 Return the assigned IP address for a given interface, if any, or [].
176 """
177 # Extract nic if passed /dev/ethX
178 if '/' in iface:
179 iface = iface.split('/')[-1]
180 if not exc_list:
181 exc_list = []
182 try:
183 inet_num = getattr(netifaces, inet_type)
184 except AttributeError:
185 raise Exception('Unknown inet type ' + str(inet_type))
186
187 interfaces = netifaces.interfaces()
188 if inc_aliases:
189 ifaces = []
190 for _iface in interfaces:
191 if iface == _iface or _iface.split(':')[0] == iface:
192 ifaces.append(_iface)
193 if fatal and not ifaces:
194 raise Exception("Invalid interface '%s'" % iface)
195 ifaces.sort()
196 else:
197 if iface not in interfaces:
198 if fatal:
199 raise Exception("%s not found " % (iface))
200 else:
201 return []
202 else:
203 ifaces = [iface]
204
205 addresses = []
206 for netiface in ifaces:
207 net_info = netifaces.ifaddresses(netiface)
208 if inet_num in net_info:
209 for entry in net_info[inet_num]:
210 if 'addr' in entry and entry['addr'] not in exc_list:
211 addresses.append(entry['addr'])
212 if fatal and not addresses:
213 raise Exception("Interface '%s' doesn't have any %s addresses." % (iface, inet_type))
214 return addresses
215
216get_ipv4_addr = partial(get_iface_addr, inet_type='AF_INET')
217
218
219def get_ipv6_addr(iface='eth0', inc_aliases=False, fatal=True, exc_list=None):
220 """
221 Return the assigned IPv6 address for a given interface, if any, or [].
222 """
223 addresses = get_iface_addr(iface=iface, inet_type='AF_INET6',
224 inc_aliases=inc_aliases, fatal=fatal,
225 exc_list=exc_list)
226 remotly_addressable = []
227 for address in addresses:
228 if not address.startswith('fe80'):
229 remotly_addressable.append(address)
230 if fatal and not remotly_addressable:
231 raise Exception("Interface '%s' doesn't have global ipv6 address." % iface)
232 return remotly_addressable
233
234
235def get_bridges(vnic_dir='/sys/devices/virtual/net'):
236 """
237 Return a list of bridges on the system or []
238 """
239 b_rgex = vnic_dir + '/*/bridge'
240 return [x.replace(vnic_dir, '').split('/')[1] for x in glob.glob(b_rgex)]
241
242
243def get_bridge_nics(bridge, vnic_dir='/sys/devices/virtual/net'):
244 """
245 Return a list of nics comprising a given bridge on the system or []
246 """
247 brif_rgex = "%s/%s/brif/*" % (vnic_dir, bridge)
248 return [x.split('/')[-1] for x in glob.glob(brif_rgex)]
249
250
251def is_bridge_member(nic):
252 """
253 Check if a given nic is a member of a bridge
254 """
255 for bridge in get_bridges():
256 if nic in get_bridge_nics(bridge):
257 return True
258 return False
157259
=== modified file 'hooks/charmhelpers/contrib/storage/linux/utils.py'
--- hooks/charmhelpers/contrib/storage/linux/utils.py 2014-07-25 08:07:41 +0000
+++ hooks/charmhelpers/contrib/storage/linux/utils.py 2014-09-21 19:34:01 +0000
@@ -46,5 +46,8 @@
46 :returns: boolean: True if the path represents a mounted device, False if46 :returns: boolean: True if the path represents a mounted device, False if
47 it doesn't.47 it doesn't.
48 '''48 '''
49 is_partition = bool(re.search(r".*[0-9]+\b", device))
49 out = check_output(['mount'])50 out = check_output(['mount'])
51 if is_partition:
52 return bool(re.search(device + r"\b", out))
50 return bool(re.search(device + r"[0-9]+\b", out))53 return bool(re.search(device + r"[0-9]+\b", out))
5154
=== modified file 'hooks/charmhelpers/core/hookenv.py'
--- hooks/charmhelpers/core/hookenv.py 2014-07-25 08:07:41 +0000
+++ hooks/charmhelpers/core/hookenv.py 2014-09-21 19:34:01 +0000
@@ -156,12 +156,15 @@
156156
157157
158class Config(dict):158class Config(dict):
159 """A Juju charm config dictionary that can write itself to159 """A dictionary representation of the charm's config.yaml, with some
160 disk (as json) and track which values have changed since160 extra features:
161 the previous hook invocation.161
162162 - See which values in the dictionary have changed since the previous hook.
163 Do not instantiate this object directly - instead call163 - For values that have changed, see what the previous value was.
164 ``hookenv.config()``164 - Store arbitrary data for use in a later hook.
165
166 NOTE: Do not instantiate this object directly - instead call
167 ``hookenv.config()``, which will return an instance of :class:`Config`.
165168
166 Example usage::169 Example usage::
167170
@@ -170,8 +173,8 @@
170 >>> config = hookenv.config()173 >>> config = hookenv.config()
171 >>> config['foo']174 >>> config['foo']
172 'bar'175 'bar'
176 >>> # store a new key/value for later use
173 >>> config['mykey'] = 'myval'177 >>> config['mykey'] = 'myval'
174 >>> config.save()
175178
176179
177 >>> # user runs `juju set mycharm foo=baz`180 >>> # user runs `juju set mycharm foo=baz`
@@ -188,22 +191,34 @@
188 >>> # keys/values that we add are preserved across hooks191 >>> # keys/values that we add are preserved across hooks
189 >>> config['mykey']192 >>> config['mykey']
190 'myval'193 'myval'
191 >>> # don't forget to save at the end of hook!
192 >>> config.save()
193194
194 """195 """
195 CONFIG_FILE_NAME = '.juju-persistent-config'196 CONFIG_FILE_NAME = '.juju-persistent-config'
196197
197 def __init__(self, *args, **kw):198 def __init__(self, *args, **kw):
198 super(Config, self).__init__(*args, **kw)199 super(Config, self).__init__(*args, **kw)
200 self.implicit_save = True
199 self._prev_dict = None201 self._prev_dict = None
200 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)202 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
201 if os.path.exists(self.path):203 if os.path.exists(self.path):
202 self.load_previous()204 self.load_previous()
203205
206 def __getitem__(self, key):
207 """For regular dict lookups, check the current juju config first,
208 then the previous (saved) copy. This ensures that user-saved values
209 will be returned by a dict lookup.
210
211 """
212 try:
213 return dict.__getitem__(self, key)
214 except KeyError:
215 return (self._prev_dict or {})[key]
216
204 def load_previous(self, path=None):217 def load_previous(self, path=None):
205 """Load previous copy of config from disk so that current values218 """Load previous copy of config from disk.
206 can be compared to previous values.219
220 In normal usage you don't need to call this method directly - it
221 is called automatically at object initialization.
207222
208 :param path:223 :param path:
209224
@@ -218,8 +233,8 @@
218 self._prev_dict = json.load(f)233 self._prev_dict = json.load(f)
219234
220 def changed(self, key):235 def changed(self, key):
221 """Return true if the value for this key has changed since236 """Return True if the current value for this key is different from
222 the last save.237 the previous value.
223238
224 """239 """
225 if self._prev_dict is None:240 if self._prev_dict is None:
@@ -228,7 +243,7 @@
228243
229 def previous(self, key):244 def previous(self, key):
230 """Return previous value for this key, or None if there245 """Return previous value for this key, or None if there
231 is no "previous" value.246 is no previous value.
232247
233 """248 """
234 if self._prev_dict:249 if self._prev_dict:
@@ -238,7 +253,13 @@
238 def save(self):253 def save(self):
239 """Save this config to disk.254 """Save this config to disk.
240255
241 Preserves items in _prev_dict that do not exist in self.256 If the charm is using the :mod:`Services Framework <services.base>`
257 or :meth:'@hook <Hooks.hook>' decorator, this
258 is called automatically at the end of successful hook execution.
259 Otherwise, it should be called directly by user code.
260
261 To disable automatic saves, set ``implicit_save=False`` on this
262 instance.
242263
243 """264 """
244 if self._prev_dict:265 if self._prev_dict:
@@ -285,8 +306,9 @@
285 raise306 raise
286307
287308
288def relation_set(relation_id=None, relation_settings={}, **kwargs):309def relation_set(relation_id=None, relation_settings=None, **kwargs):
289 """Set relation information for the current unit"""310 """Set relation information for the current unit"""
311 relation_settings = relation_settings if relation_settings else {}
290 relation_cmd_line = ['relation-set']312 relation_cmd_line = ['relation-set']
291 if relation_id is not None:313 if relation_id is not None:
292 relation_cmd_line.extend(('-r', relation_id))314 relation_cmd_line.extend(('-r', relation_id))
@@ -477,6 +499,9 @@
477 hook_name = os.path.basename(args[0])499 hook_name = os.path.basename(args[0])
478 if hook_name in self._hooks:500 if hook_name in self._hooks:
479 self._hooks[hook_name]()501 self._hooks[hook_name]()
502 cfg = config()
503 if cfg.implicit_save:
504 cfg.save()
480 else:505 else:
481 raise UnregisteredHookError(hook_name)506 raise UnregisteredHookError(hook_name)
482507
483508
=== modified file 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py 2014-07-25 08:07:41 +0000
+++ hooks/charmhelpers/core/host.py 2014-09-21 19:34:01 +0000
@@ -12,6 +12,8 @@
12import string12import string
13import subprocess13import subprocess
14import hashlib14import hashlib
15import shutil
16from contextlib import contextmanager
1517
16from collections import OrderedDict18from collections import OrderedDict
1719
@@ -52,7 +54,7 @@
52def service_running(service):54def service_running(service):
53 """Determine whether a system service is running"""55 """Determine whether a system service is running"""
54 try:56 try:
55 output = subprocess.check_output(['service', service, 'status'])57 output = subprocess.check_output(['service', service, 'status'], stderr=subprocess.STDOUT)
56 except subprocess.CalledProcessError:58 except subprocess.CalledProcessError:
57 return False59 return False
58 else:60 else:
@@ -62,6 +64,16 @@
62 return False64 return False
6365
6466
67def service_available(service_name):
68 """Determine whether a system service is available"""
69 try:
70 subprocess.check_output(['service', service_name, 'status'], stderr=subprocess.STDOUT)
71 except subprocess.CalledProcessError:
72 return False
73 else:
74 return True
75
76
65def adduser(username, password=None, shell='/bin/bash', system_user=False):77def adduser(username, password=None, shell='/bin/bash', system_user=False):
66 """Add a user to the system"""78 """Add a user to the system"""
67 try:79 try:
@@ -320,12 +332,29 @@
320332
321 '''333 '''
322 import apt_pkg334 import apt_pkg
335 from charmhelpers.fetch import apt_cache
323 if not pkgcache:336 if not pkgcache:
324 apt_pkg.init()337 pkgcache = apt_cache()
325 # Force Apt to build its cache in memory. That way we avoid race
326 # conditions with other applications building the cache in the same
327 # place.
328 apt_pkg.config.set("Dir::Cache::pkgcache", "")
329 pkgcache = apt_pkg.Cache()
330 pkg = pkgcache[package]338 pkg = pkgcache[package]
331 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)339 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
340
341
342@contextmanager
343def chdir(d):
344 cur = os.getcwd()
345 try:
346 yield os.chdir(d)
347 finally:
348 os.chdir(cur)
349
350
351def chownr(path, owner, group):
352 uid = pwd.getpwnam(owner).pw_uid
353 gid = grp.getgrnam(group).gr_gid
354
355 for root, dirs, files in os.walk(path):
356 for name in dirs + files:
357 full = os.path.join(root, name)
358 broken_symlink = os.path.lexists(full) and not os.path.exists(full)
359 if not broken_symlink:
360 os.chown(full, uid, gid)
332361
=== added directory 'hooks/charmhelpers/core/services'
=== added file 'hooks/charmhelpers/core/services/__init__.py'
--- hooks/charmhelpers/core/services/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/services/__init__.py 2014-09-21 19:34:01 +0000
@@ -0,0 +1,2 @@
1from .base import *
2from .helpers import *
03
=== added file 'hooks/charmhelpers/core/services/base.py'
--- hooks/charmhelpers/core/services/base.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/services/base.py 2014-09-21 19:34:01 +0000
@@ -0,0 +1,313 @@
1import os
2import re
3import json
4from collections import Iterable
5
6from charmhelpers.core import host
7from charmhelpers.core import hookenv
8
9
10__all__ = ['ServiceManager', 'ManagerCallback',
11 'PortManagerCallback', 'open_ports', 'close_ports', 'manage_ports',
12 'service_restart', 'service_stop']
13
14
15class ServiceManager(object):
16 def __init__(self, services=None):
17 """
18 Register a list of services, given their definitions.
19
20 Service definitions are dicts in the following formats (all keys except
21 'service' are optional)::
22
23 {
24 "service": <service name>,
25 "required_data": <list of required data contexts>,
26 "provided_data": <list of provided data contexts>,
27 "data_ready": <one or more callbacks>,
28 "data_lost": <one or more callbacks>,
29 "start": <one or more callbacks>,
30 "stop": <one or more callbacks>,
31 "ports": <list of ports to manage>,
32 }
33
34 The 'required_data' list should contain dicts of required data (or
35 dependency managers that act like dicts and know how to collect the data).
36 Only when all items in the 'required_data' list are populated are the list
37 of 'data_ready' and 'start' callbacks executed. See `is_ready()` for more
38 information.
39
40 The 'provided_data' list should contain relation data providers, most likely
41 a subclass of :class:`charmhelpers.core.services.helpers.RelationContext`,
42 that will indicate a set of data to set on a given relation.
43
44 The 'data_ready' value should be either a single callback, or a list of
45 callbacks, to be called when all items in 'required_data' pass `is_ready()`.
46 Each callback will be called with the service name as the only parameter.
47 After all of the 'data_ready' callbacks are called, the 'start' callbacks
48 are fired.
49
50 The 'data_lost' value should be either a single callback, or a list of
51 callbacks, to be called when a 'required_data' item no longer passes
52 `is_ready()`. Each callback will be called with the service name as the
53 only parameter. After all of the 'data_lost' callbacks are called,
54 the 'stop' callbacks are fired.
55
56 The 'start' value should be either a single callback, or a list of
57 callbacks, to be called when starting the service, after the 'data_ready'
58 callbacks are complete. Each callback will be called with the service
59 name as the only parameter. This defaults to
60 `[host.service_start, services.open_ports]`.
61
62 The 'stop' value should be either a single callback, or a list of
63 callbacks, to be called when stopping the service. If the service is
64 being stopped because it no longer has all of its 'required_data', this
65 will be called after all of the 'data_lost' callbacks are complete.
66 Each callback will be called with the service name as the only parameter.
67 This defaults to `[services.close_ports, host.service_stop]`.
68
69 The 'ports' value should be a list of ports to manage. The default
70 'start' handler will open the ports after the service is started,
71 and the default 'stop' handler will close the ports prior to stopping
72 the service.
73
74
75 Examples:
76
77 The following registers an Upstart service called bingod that depends on
78 a mongodb relation and which runs a custom `db_migrate` function prior to
79 restarting the service, and a Runit service called spadesd::
80
81 manager = services.ServiceManager([
82 {
83 'service': 'bingod',
84 'ports': [80, 443],
85 'required_data': [MongoRelation(), config(), {'my': 'data'}],
86 'data_ready': [
87 services.template(source='bingod.conf'),
88 services.template(source='bingod.ini',
89 target='/etc/bingod.ini',
90 owner='bingo', perms=0400),
91 ],
92 },
93 {
94 'service': 'spadesd',
95 'data_ready': services.template(source='spadesd_run.j2',
96 target='/etc/sv/spadesd/run',
97 perms=0555),
98 'start': runit_start,
99 'stop': runit_stop,
100 },
101 ])
102 manager.manage()
103 """
104 self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
105 self._ready = None
106 self.services = {}
107 for service in services or []:
108 service_name = service['service']
109 self.services[service_name] = service
110
111 def manage(self):
112 """
113 Handle the current hook by doing The Right Thing with the registered services.
114 """
115 hook_name = hookenv.hook_name()
116 if hook_name == 'stop':
117 self.stop_services()
118 else:
119 self.provide_data()
120 self.reconfigure_services()
121 cfg = hookenv.config()
122 if cfg.implicit_save:
123 cfg.save()
124
125 def provide_data(self):
126 """
127 Set the relation data for each provider in the ``provided_data`` list.
128
129 A provider must have a `name` attribute, which indicates which relation
130 to set data on, and a `provide_data()` method, which returns a dict of
131 data to set.
132 """
133 hook_name = hookenv.hook_name()
134 for service in self.services.values():
135 for provider in service.get('provided_data', []):
136 if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
137 data = provider.provide_data()
138 _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data
139 if _ready:
140 hookenv.relation_set(None, data)
141
142 def reconfigure_services(self, *service_names):
143 """
144 Update all files for one or more registered services, and,
145 if ready, optionally restart them.
146
147 If no service names are given, reconfigures all registered services.
148 """
149 for service_name in service_names or self.services.keys():
150 if self.is_ready(service_name):
151 self.fire_event('data_ready', service_name)
152 self.fire_event('start', service_name, default=[
153 service_restart,
154 manage_ports])
155 self.save_ready(service_name)
156 else:
157 if self.was_ready(service_name):
158 self.fire_event('data_lost', service_name)
159 self.fire_event('stop', service_name, default=[
160 manage_ports,
161 service_stop])
162 self.save_lost(service_name)
163
164 def stop_services(self, *service_names):
165 """
166 Stop one or more registered services, by name.
167
168 If no service names are given, stops all registered services.
169 """
170 for service_name in service_names or self.services.keys():
171 self.fire_event('stop', service_name, default=[
172 manage_ports,
173 service_stop])
174
175 def get_service(self, service_name):
176 """
177 Given the name of a registered service, return its service definition.
178 """
179 service = self.services.get(service_name)
180 if not service:
181 raise KeyError('Service not registered: %s' % service_name)
182 return service
183
184 def fire_event(self, event_name, service_name, default=None):
185 """
186 Fire a data_ready, data_lost, start, or stop event on a given service.
187 """
188 service = self.get_service(service_name)
189 callbacks = service.get(event_name, default)
190 if not callbacks:
191 return
192 if not isinstance(callbacks, Iterable):
193 callbacks = [callbacks]
194 for callback in callbacks:
195 if isinstance(callback, ManagerCallback):
196 callback(self, service_name, event_name)
197 else:
198 callback(service_name)
199
200 def is_ready(self, service_name):
201 """
202 Determine if a registered service is ready, by checking its 'required_data'.
203
204 A 'required_data' item can be any mapping type, and is considered ready
205 if `bool(item)` evaluates as True.
206 """
207 service = self.get_service(service_name)
208 reqs = service.get('required_data', [])
209 return all(bool(req) for req in reqs)
210
211 def _load_ready_file(self):
212 if self._ready is not None:
213 return
214 if os.path.exists(self._ready_file):
215 with open(self._ready_file) as fp:
216 self._ready = set(json.load(fp))
217 else:
218 self._ready = set()
219
220 def _save_ready_file(self):
221 if self._ready is None:
222 return
223 with open(self._ready_file, 'w') as fp:
224 json.dump(list(self._ready), fp)
225
226 def save_ready(self, service_name):
227 """
228 Save an indicator that the given service is now data_ready.
229 """
230 self._load_ready_file()
231 self._ready.add(service_name)
232 self._save_ready_file()
233
234 def save_lost(self, service_name):
235 """
236 Save an indicator that the given service is no longer data_ready.
237 """
238 self._load_ready_file()
239 self._ready.discard(service_name)
240 self._save_ready_file()
241
242 def was_ready(self, service_name):
243 """
244 Determine if the given service was previously data_ready.
245 """
246 self._load_ready_file()
247 return service_name in self._ready
248
249
250class ManagerCallback(object):
251 """
252 Special case of a callback that takes the `ServiceManager` instance
253 in addition to the service name.
254
255 Subclasses should implement `__call__` which should accept three parameters:
256
257 * `manager` The `ServiceManager` instance
258 * `service_name` The name of the service it's being triggered for
259 * `event_name` The name of the event that this callback is handling
260 """
261 def __call__(self, manager, service_name, event_name):
262 raise NotImplementedError()
263
264
265class PortManagerCallback(ManagerCallback):
266 """
267 Callback class that will open or close ports, for use as either
268 a start or stop action.
269 """
270 def __call__(self, manager, service_name, event_name):
271 service = manager.get_service(service_name)
272 new_ports = service.get('ports', [])
273 port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name))
274 if os.path.exists(port_file):
275 with open(port_file) as fp:
276 old_ports = fp.read().split(',')
277 for old_port in old_ports:
278 if bool(old_port):
279 old_port = int(old_port)
280 if old_port not in new_ports:
281 hookenv.close_port(old_port)
282 with open(port_file, 'w') as fp:
283 fp.write(','.join(str(port) for port in new_ports))
284 for port in new_ports:
285 if event_name == 'start':
286 hookenv.open_port(port)
287 elif event_name == 'stop':
288 hookenv.close_port(port)
289
290
291def service_stop(service_name):
292 """
293 Wrapper around host.service_stop to prevent spurious "unknown service"
294 messages in the logs.
295 """
296 if host.service_running(service_name):
297 host.service_stop(service_name)
298
299
300def service_restart(service_name):
301 """
302 Wrapper around host.service_restart to prevent spurious "unknown service"
303 messages in the logs.
304 """
305 if host.service_available(service_name):
306 if host.service_running(service_name):
307 host.service_restart(service_name)
308 else:
309 host.service_start(service_name)
310
311
312# Convenience aliases
313open_ports = close_ports = manage_ports = PortManagerCallback()
0314
=== added file 'hooks/charmhelpers/core/services/helpers.py'
--- hooks/charmhelpers/core/services/helpers.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/services/helpers.py 2014-09-21 19:34:01 +0000
@@ -0,0 +1,125 @@
1from charmhelpers.core import hookenv
2from charmhelpers.core import templating
3
4from charmhelpers.core.services.base import ManagerCallback
5
6
7__all__ = ['RelationContext', 'TemplateCallback',
8 'render_template', 'template']
9
10
11class RelationContext(dict):
12 """
13 Base class for a context generator that gets relation data from juju.
14
15 Subclasses must provide the attributes `name`, which is the name of the
16 interface of interest, `interface`, which is the type of the interface of
17 interest, and `required_keys`, which is the set of keys required for the
18 relation to be considered complete. The data for all interfaces matching
19 the `name` attribute that are complete will used to populate the dictionary
20 values (see `get_data`, below).
21
22 The generated context will be namespaced under the interface type, to prevent
23 potential naming conflicts.
24 """
25 name = None
26 interface = None
27 required_keys = []
28
29 def __init__(self, *args, **kwargs):
30 super(RelationContext, self).__init__(*args, **kwargs)
31 self.get_data()
32
33 def __bool__(self):
34 """
35 Returns True if all of the required_keys are available.
36 """
37 return self.is_ready()
38
39 __nonzero__ = __bool__
40
41 def __repr__(self):
42 return super(RelationContext, self).__repr__()
43
44 def is_ready(self):
45 """
46 Returns True if all of the `required_keys` are available from any units.
47 """
48 ready = len(self.get(self.name, [])) > 0
49 if not ready:
50 hookenv.log('Incomplete relation: {}'.format(self.__class__.__name__), hookenv.DEBUG)
51 return ready
52
53 def _is_ready(self, unit_data):
54 """
55 Helper method that tests a set of relation data and returns True if
56 all of the `required_keys` are present.
57 """
58 return set(unit_data.keys()).issuperset(set(self.required_keys))
59
60 def get_data(self):
61 """
62 Retrieve the relation data for each unit involved in a relation and,
63 if complete, store it in a list under `self[self.name]`. This
64 is automatically called when the RelationContext is instantiated.
65
66 The units are sorted lexographically first by the service ID, then by
67 the unit ID. Thus, if an interface has two other services, 'db:1'
68 and 'db:2', with 'db:1' having two units, 'wordpress/0' and 'wordpress/1',
69 and 'db:2' having one unit, 'mediawiki/0', all of which have a complete
70 set of data, the relation data for the units will be stored in the
71 order: 'wordpress/0', 'wordpress/1', 'mediawiki/0'.
72
73 If you only care about a single unit on the relation, you can just
74 access it as `{{ interface[0]['key'] }}`. However, if you can at all
75 support multiple units on a relation, you should iterate over the list,
76 like::
77
78 {% for unit in interface -%}
79 {{ unit['key'] }}{% if not loop.last %},{% endif %}
80 {%- endfor %}
81
82 Note that since all sets of relation data from all related services and
83 units are in a single list, if you need to know which service or unit a
84 set of data came from, you'll need to extend this class to preserve
85 that information.
86 """
87 if not hookenv.relation_ids(self.name):
88 return
89
90 ns = self.setdefault(self.name, [])
91 for rid in sorted(hookenv.relation_ids(self.name)):
92 for unit in sorted(hookenv.related_units(rid)):
93 reldata = hookenv.relation_get(rid=rid, unit=unit)
94 if self._is_ready(reldata):
95 ns.append(reldata)
96
97 def provide_data(self):
98 """
99 Return data to be relation_set for this interface.
100 """
101 return {}
102
103
104class TemplateCallback(ManagerCallback):
105 """
106 Callback class that will render a template, for use as a ready action.
107 """
108 def __init__(self, source, target, owner='root', group='root', perms=0444):
109 self.source = source
110 self.target = target
111 self.owner = owner
112 self.group = group
113 self.perms = perms
114
115 def __call__(self, manager, service_name, event_name):
116 service = manager.get_service(service_name)
117 context = {}
118 for ctx in service.get('required_data', []):
119 context.update(ctx)
120 templating.render(self.source, self.target, context,
121 self.owner, self.group, self.perms)
122
123
124# Convenience aliases for templates
125render_template = template = TemplateCallback
0126
=== added file 'hooks/charmhelpers/core/templating.py'
--- hooks/charmhelpers/core/templating.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/templating.py 2014-09-21 19:34:01 +0000
@@ -0,0 +1,51 @@
1import os
2
3from charmhelpers.core import host
4from charmhelpers.core import hookenv
5
6
7def render(source, target, context, owner='root', group='root', perms=0444, templates_dir=None):
8 """
9 Render a template.
10
11 The `source` path, if not absolute, is relative to the `templates_dir`.
12
13 The `target` path should be absolute.
14
15 The context should be a dict containing the values to be replaced in the
16 template.
17
18 The `owner`, `group`, and `perms` options will be passed to `write_file`.
19
20 If omitted, `templates_dir` defaults to the `templates` folder in the charm.
21
22 Note: Using this requires python-jinja2; if it is not installed, calling
23 this will attempt to use charmhelpers.fetch.apt_install to install it.
24 """
25 try:
26 from jinja2 import FileSystemLoader, Environment, exceptions
27 except ImportError:
28 try:
29 from charmhelpers.fetch import apt_install
30 except ImportError:
31 hookenv.log('Could not import jinja2, and could not import '
32 'charmhelpers.fetch to install it',
33 level=hookenv.ERROR)
34 raise
35 apt_install('python-jinja2', fatal=True)
36 from jinja2 import FileSystemLoader, Environment, exceptions
37
38 if templates_dir is None:
39 templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
40 loader = Environment(loader=FileSystemLoader(templates_dir))
41 try:
42 source = source
43 template = loader.get_template(source)
44 except exceptions.TemplateNotFound as e:
45 hookenv.log('Could not load template %s from %s.' %
46 (source, templates_dir),
47 level=hookenv.ERROR)
48 raise e
49 content = template.render(context)
50 host.mkdir(os.path.dirname(target))
51 host.write_file(target, content, owner, group, perms)
052
=== modified file 'hooks/charmhelpers/fetch/__init__.py'
--- hooks/charmhelpers/fetch/__init__.py 2014-07-25 08:07:41 +0000
+++ hooks/charmhelpers/fetch/__init__.py 2014-09-21 19:34:01 +0000
@@ -1,4 +1,5 @@
1import importlib1import importlib
2from tempfile import NamedTemporaryFile
2import time3import time
3from yaml import safe_load4from yaml import safe_load
4from charmhelpers.core.host import (5from charmhelpers.core.host import (
@@ -116,14 +117,7 @@
116117
117def filter_installed_packages(packages):118def filter_installed_packages(packages):
118 """Returns a list of packages that require installation"""119 """Returns a list of packages that require installation"""
119 import apt_pkg120 cache = apt_cache()
120 apt_pkg.init()
121
122 # Tell apt to build an in-memory cache to prevent race conditions (if
123 # another process is already building the cache).
124 apt_pkg.config.set("Dir::Cache::pkgcache", "")
125
126 cache = apt_pkg.Cache()
127 _pkgs = []121 _pkgs = []
128 for package in packages:122 for package in packages:
129 try:123 try:
@@ -136,6 +130,16 @@
136 return _pkgs130 return _pkgs
137131
138132
133def apt_cache(in_memory=True):
134 """Build and return an apt cache"""
135 import apt_pkg
136 apt_pkg.init()
137 if in_memory:
138 apt_pkg.config.set("Dir::Cache::pkgcache", "")
139 apt_pkg.config.set("Dir::Cache::srcpkgcache", "")
140 return apt_pkg.Cache()
141
142
139def apt_install(packages, options=None, fatal=False):143def apt_install(packages, options=None, fatal=False):
140 """Install one or more packages"""144 """Install one or more packages"""
141 if options is None:145 if options is None:
@@ -201,6 +205,27 @@
201205
202206
203def add_source(source, key=None):207def add_source(source, key=None):
208 """Add a package source to this system.
209
210 @param source: a URL or sources.list entry, as supported by
211 add-apt-repository(1). Examples:
212 ppa:charmers/example
213 deb https://stub:key@private.example.com/ubuntu trusty main
214
215 In addition:
216 'proposed:' may be used to enable the standard 'proposed'
217 pocket for the release.
218 'cloud:' may be used to activate official cloud archive pockets,
219 such as 'cloud:icehouse'
220
221 @param key: A key to be added to the system's APT keyring and used
222 to verify the signatures on packages. Ideally, this should be an
223 ASCII format GPG public key including the block headers. A GPG key
224 id may also be used, but be aware that only insecure protocols are
225 available to retrieve the actual public key from a public keyserver
226 placing your Juju environment at risk. ppa and cloud archive keys
227 are securely added automtically, so sould not be provided.
228 """
204 if source is None:229 if source is None:
205 log('Source is not present. Skipping')230 log('Source is not present. Skipping')
206 return231 return
@@ -225,10 +250,23 @@
225 release = lsb_release()['DISTRIB_CODENAME']250 release = lsb_release()['DISTRIB_CODENAME']
226 with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:251 with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
227 apt.write(PROPOSED_POCKET.format(release))252 apt.write(PROPOSED_POCKET.format(release))
253 else:
254 raise SourceConfigError("Unknown source: {!r}".format(source))
255
228 if key:256 if key:
229 subprocess.check_call(['apt-key', 'adv', '--keyserver',257 if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
230 'hkp://keyserver.ubuntu.com:80', '--recv',258 with NamedTemporaryFile() as key_file:
231 key])259 key_file.write(key)
260 key_file.flush()
261 key_file.seek(0)
262 subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)
263 else:
264 # Note that hkp: is in no way a secure protocol. Using a
265 # GPG key id is pointless from a security POV unless you
266 # absolutely trust your network and DNS.
267 subprocess.check_call(['apt-key', 'adv', '--keyserver',
268 'hkp://keyserver.ubuntu.com:80', '--recv',
269 key])
232270
233271
234def configure_sources(update=False,272def configure_sources(update=False,
@@ -238,7 +276,8 @@
238 Configure multiple sources from charm configuration.276 Configure multiple sources from charm configuration.
239277
240 The lists are encoded as yaml fragments in the configuration.278 The lists are encoded as yaml fragments in the configuration.
241 The frament needs to be included as a string.279 The frament needs to be included as a string. Sources and their
280 corresponding keys are of the types supported by add_source().
242281
243 Example config:282 Example config:
244 install_sources: |283 install_sources: |
245284
=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
--- hooks/charmhelpers/fetch/archiveurl.py 2014-04-16 08:37:56 +0000
+++ hooks/charmhelpers/fetch/archiveurl.py 2014-09-21 19:34:01 +0000
@@ -1,6 +1,8 @@
1import os1import os
2import urllib22import urllib2
3from urllib import urlretrieve
3import urlparse4import urlparse
5import hashlib
46
5from charmhelpers.fetch import (7from charmhelpers.fetch import (
6 BaseFetchHandler,8 BaseFetchHandler,
@@ -12,7 +14,17 @@
12)14)
13from charmhelpers.core.host import mkdir15from charmhelpers.core.host import mkdir
1416
1517"""
18This class is a plugin for charmhelpers.fetch.install_remote.
19
20It 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/.
21
22Example usage:
23install_remote("https://example.com/some/archive.tar.gz")
24# Installs the contents of archive.tar.gz in $CHARM_DIR/fetched/.
25
26See charmhelpers.fetch.archiveurl.get_archivehandler for supported archive types.
27"""
16class ArchiveUrlFetchHandler(BaseFetchHandler):28class ArchiveUrlFetchHandler(BaseFetchHandler):
17 """Handler for archives via generic URLs"""29 """Handler for archives via generic URLs"""
18 def can_handle(self, source):30 def can_handle(self, source):
@@ -61,3 +73,31 @@
61 except OSError as e:73 except OSError as e:
62 raise UnhandledSource(e.strerror)74 raise UnhandledSource(e.strerror)
63 return extract(dld_file)75 return extract(dld_file)
76
77 # Mandatory file validation via Sha1 or MD5 hashing.
78 def download_and_validate(self, url, hashsum, validate="sha1"):
79 if validate == 'sha1' and len(hashsum) != 40:
80 raise ValueError("HashSum must be = 40 characters when using sha1"
81 " validation")
82 if validate == 'md5' and len(hashsum) != 32:
83 raise ValueError("HashSum must be = 32 characters when using md5"
84 " validation")
85 tempfile, headers = urlretrieve(url)
86 self.validate_file(tempfile, hashsum, validate)
87 return tempfile
88
89 # Predicate method that returns status of hash matching expected hash.
90 def validate_file(self, source, hashsum, vmethod='sha1'):
91 if vmethod != 'sha1' and vmethod != 'md5':
92 raise ValueError("Validation Method not supported")
93
94 if vmethod == 'md5':
95 m = hashlib.md5()
96 if vmethod == 'sha1':
97 m = hashlib.sha1()
98 with open(source) as f:
99 for line in f:
100 m.update(line)
101 if hashsum != m.hexdigest():
102 msg = "Hash Mismatch on {} expected {} got {}"
103 raise ValueError(msg.format(source, hashsum, m.hexdigest()))
64104
=== modified file 'hooks/hooks.py'
--- hooks/hooks.py 2014-08-15 09:06:49 +0000
+++ hooks/hooks.py 2014-09-21 19:34:01 +0000
@@ -15,14 +15,17 @@
15import ceph15import ceph
16from charmhelpers.core.hookenv import (16from charmhelpers.core.hookenv import (
17 log,17 log,
18 WARNING,
18 ERROR,19 ERROR,
19 config,20 config,
20 relation_ids,21 relation_ids,
21 related_units,22 related_units,
22 relation_get,23 relation_get,
24 relation_set,
23 Hooks,25 Hooks,
24 UnregisteredHookError,26 UnregisteredHookError,
25 service_name27 service_name,
28 unit_get
26)29)
27from charmhelpers.core.host import (30from charmhelpers.core.host import (
28 umount,31 umount,
@@ -39,10 +42,14 @@
39from utils import (42from utils import (
40 render_template,43 render_template,
41 get_host_ip,44 get_host_ip,
45 setup_ipv6
42)46)
4347
44from charmhelpers.contrib.openstack.alternatives import install_alternative48from charmhelpers.contrib.openstack.alternatives import install_alternative
45from charmhelpers.contrib.network.ip import is_ipv649from charmhelpers.contrib.network.ip import (
50 is_ipv6,
51 get_ipv6_addr
52)
4653
47hooks = Hooks()54hooks = Hooks()
4855
@@ -58,6 +65,10 @@
58def install():65def install():
59 add_source(config('source'), config('key'))66 add_source(config('source'), config('key'))
60 apt_update(fatal=True)67 apt_update(fatal=True)
68
69 if config('prefer-ipv6'):
70 setup_ipv6()
71
61 apt_install(packages=ceph.PACKAGES, fatal=True)72 apt_install(packages=ceph.PACKAGES, fatal=True)
62 install_upstart_scripts()73 install_upstart_scripts()
6374
@@ -76,6 +87,14 @@
76 'ceph_public_network': config('ceph-public-network'),87 'ceph_public_network': config('ceph-public-network'),
77 'ceph_cluster_network': config('ceph-cluster-network'),88 'ceph_cluster_network': config('ceph-cluster-network'),
78 }89 }
90
91 if config('prefer-ipv6'):
92 host_ip = get_ipv6_addr()[0]
93 if host_ip:
94 cephcontext['host_ip'] = host_ip
95 else:
96 log("Unable to obtain host address", level=WARNING)
97
79 # Install ceph.conf as an alternative to support98 # Install ceph.conf as an alternative to support
80 # co-existence with other charms that write this file99 # co-existence with other charms that write this file
81 charm_ceph_conf = "/var/lib/charm/{}/ceph.conf".format(service_name())100 charm_ceph_conf = "/var/lib/charm/{}/ceph.conf".format(service_name())
@@ -95,6 +114,9 @@
95 log('Invalid OSD disk format configuration specified', level=ERROR)114 log('Invalid OSD disk format configuration specified', level=ERROR)
96 sys.exit(1)115 sys.exit(1)
97116
117 if config('prefer-ipv6'):
118 setup_ipv6()
119
98 e_mountpoint = config('ephemeral-unmount')120 e_mountpoint = config('ephemeral-unmount')
99 if (e_mountpoint and ceph.filesystem_mounted(e_mountpoint)):121 if (e_mountpoint and ceph.filesystem_mounted(e_mountpoint)):
100 umount(e_mountpoint)122 umount(e_mountpoint)
@@ -120,8 +142,13 @@
120 hosts = []142 hosts = []
121 for relid in relation_ids('mon'):143 for relid in relation_ids('mon'):
122 for unit in related_units(relid):144 for unit in related_units(relid):
123 addr = relation_get('ceph-public-address', unit, relid) or \145 addr = relation_get('ceph-public-address', unit, relid)
124 get_host_ip(relation_get('private-address', unit, relid))146 if not addr:
147 if config('prefer-ipv6'):
148 addr = relation_get('private-address', unit, relid)
149 else:
150 get_host_ip(relation_get('private-address', unit, relid))
151
125 if addr is not None:152 if addr is not None:
126 if is_ipv6(addr):153 if is_ipv6(addr):
127 hosts.append('[{}]:6789'.format(addr))154 hosts.append('[{}]:6789'.format(addr))
@@ -166,6 +193,17 @@
166@hooks.hook('mon-relation-changed',193@hooks.hook('mon-relation-changed',
167 'mon-relation-departed')194 'mon-relation-departed')
168def mon_relation():195def mon_relation():
196 if config('prefer-ipv6'):
197 host = get_ipv6_addr()[0]
198 else:
199 host = unit_get('private-address')
200
201 if host:
202 relation_data = {'private-address': host}
203 relation_set(**relation_data)
204 else:
205 log("Unable to obtain host address", level=WARNING)
206
169 bootstrap_key = relation_get('osd_bootstrap_key')207 bootstrap_key = relation_get('osd_bootstrap_key')
170 if get_fsid() and get_auth() and bootstrap_key:208 if get_fsid() and get_auth() and bootstrap_key:
171 log('mon has provided conf- scanning disks')209 log('mon has provided conf- scanning disks')
172210
=== modified file 'hooks/utils.py'
--- hooks/utils.py 2013-10-10 10:49:36 +0000
+++ hooks/utils.py 2014-09-21 19:34:01 +0000
@@ -18,6 +18,10 @@
18 filter_installed_packages18 filter_installed_packages
19)19)
2020
21from charmhelpers.core.host import (
22 lsb_release
23)
24
21TEMPLATES_DIR = 'templates'25TEMPLATES_DIR = 'templates'
2226
23try:27try:
@@ -72,3 +76,10 @@
72 answers = dns.resolver.query(hostname, 'A')76 answers = dns.resolver.query(hostname, 'A')
73 if answers:77 if answers:
74 return answers[0].address78 return answers[0].address
79
80
81def setup_ipv6():
82 ubuntu_rel = float(lsb_release()['DISTRIB_RELEASE'])
83 if ubuntu_rel < 14.04:
84 raise Exception("IPv6 is not supported for Ubuntu "
85 "versions less than Trusty 14.04")
7586
=== modified file 'templates/ceph.conf'
--- templates/ceph.conf 2014-07-25 08:07:41 +0000
+++ templates/ceph.conf 2014-09-21 19:34:01 +0000
@@ -32,3 +32,6 @@
32 osd journal size = {{ osd_journal_size }}32 osd journal size = {{ osd_journal_size }}
33 filestore xattr use omap = true33 filestore xattr use omap = true
3434
35 host = {{ hostname }}
36 public addr = {{ host_ip }}
37 cluster addr = {{ host_ip }}
35\ No newline at end of file38\ No newline at end of file

Subscribers

People subscribed via source and target branches