Merge lp:~hopem/charms/trusty/ceph-osd/support-ipv6 into lp:~openstack-charmers-archive/charms/trusty/ceph-osd/next
- Trusty Tahr (14.04)
- support-ipv6
- Merge into 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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Xiang Hui | Pending | ||
OpenStack Charmers | Pending | ||
Review via email: mp+235189@code.launchpad.net |
Commit message
Description of the change
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 |