Merge lp:~openstack-charmers/charms/precise/glance/ceilometer-support into lp:~charmers/charms/precise/glance/trunk
- Precise Pangolin (12.04)
- ceilometer-support
- Merge into trunk
Proposed by
James Page
Status: | Merged |
---|---|
Merged at revision: | 41 |
Proposed branch: | lp:~openstack-charmers/charms/precise/glance/ceilometer-support |
Merge into: | lp:~charmers/charms/precise/glance/trunk |
Diff against target: |
1065 lines (+403/-99) 15 files modified
config.yaml (+8/-10) hooks/charmhelpers/contrib/openstack/context.py (+20/-1) hooks/charmhelpers/contrib/openstack/neutron.py (+20/-0) hooks/charmhelpers/contrib/openstack/utils.py (+81/-10) hooks/charmhelpers/contrib/storage/linux/ceph.py (+27/-3) hooks/charmhelpers/core/hookenv.py (+78/-23) hooks/charmhelpers/core/host.py (+15/-9) hooks/charmhelpers/fetch/__init__.py (+53/-5) hooks/charmhelpers/fetch/bzrurl.py (+1/-1) hooks/glance_contexts.py (+1/-10) hooks/glance_relations.py (+15/-1) hooks/glance_utils.py (+1/-0) metadata.yaml (+2/-0) templates/folsom/glance-api.conf (+9/-25) unit_tests/test_glance_relations.py (+72/-1) |
To merge this branch: | bzr merge lp:~openstack-charmers/charms/precise/glance/ceilometer-support |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
charmers | Pending | ||
Review via email:
|
Commit message
Description of the change
Support for use with ceilometer
To post a comment you must log in.
- 43. By Adam Gandelman
-
Sync helpers, use is_relation_made() from helpers.
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 2013-09-26 09:49:19 +0000 |
3 | +++ config.yaml 2013-11-06 04:26:29 +0000 |
4 | @@ -70,14 +70,12 @@ |
5 | ssl_key: |
6 | type: string |
7 | description: SSL key to use with certificate specified as ssl_cert. |
8 | - ceph-osd-replication-count: |
9 | - default: 2 |
10 | - type: int |
11 | - description: | |
12 | - This value dictates the number of replicas ceph must make of any |
13 | - object it stores within the images rbd pool. Of course, this only |
14 | - applies if using Ceph as a backend store. Note that once the images |
15 | - rbd pool has been created, changing this value will not have any |
16 | - effect (although it can be changed in ceph by manually configuring |
17 | - your ceph cluster). |
18 | + rabbit-user: |
19 | + default: glance |
20 | + type: string |
21 | + description: Username to request access on rabbitmq-server. |
22 | + rabbit-vhost: |
23 | + default: openstack |
24 | + type: string |
25 | + description: RabbitMQ virtual host to request access on rabbitmq-server. |
26 | |
27 | |
28 | === added symlink 'hooks/amqp-relation-changed' |
29 | === target is u'glance_relations.py' |
30 | === added symlink 'hooks/amqp-relation-joined' |
31 | === target is u'glance_relations.py' |
32 | === modified file 'hooks/charmhelpers/contrib/openstack/context.py' |
33 | --- hooks/charmhelpers/contrib/openstack/context.py 2013-10-15 01:32:42 +0000 |
34 | +++ hooks/charmhelpers/contrib/openstack/context.py 2013-11-06 04:26:29 +0000 |
35 | @@ -385,16 +385,33 @@ |
36 | def ovs_ctxt(self): |
37 | driver = neutron_plugin_attribute(self.plugin, 'driver', |
38 | self.network_manager) |
39 | - |
40 | + config = neutron_plugin_attribute(self.plugin, 'config', |
41 | + self.network_manager) |
42 | ovs_ctxt = { |
43 | 'core_plugin': driver, |
44 | 'neutron_plugin': 'ovs', |
45 | 'neutron_security_groups': self.neutron_security_groups, |
46 | 'local_ip': unit_private_ip(), |
47 | + 'config': config |
48 | } |
49 | |
50 | return ovs_ctxt |
51 | |
52 | + def nvp_ctxt(self): |
53 | + driver = neutron_plugin_attribute(self.plugin, 'driver', |
54 | + self.network_manager) |
55 | + config = neutron_plugin_attribute(self.plugin, 'config', |
56 | + self.network_manager) |
57 | + nvp_ctxt = { |
58 | + 'core_plugin': driver, |
59 | + 'neutron_plugin': 'nvp', |
60 | + 'neutron_security_groups': self.neutron_security_groups, |
61 | + 'local_ip': unit_private_ip(), |
62 | + 'config': config |
63 | + } |
64 | + |
65 | + return nvp_ctxt |
66 | + |
67 | def __call__(self): |
68 | self._ensure_packages() |
69 | |
70 | @@ -408,6 +425,8 @@ |
71 | |
72 | if self.plugin == 'ovs': |
73 | ctxt.update(self.ovs_ctxt()) |
74 | + elif self.plugin == 'nvp': |
75 | + ctxt.update(self.nvp_ctxt()) |
76 | |
77 | self._save_flag_file() |
78 | return ctxt |
79 | |
80 | === modified file 'hooks/charmhelpers/contrib/openstack/neutron.py' |
81 | --- hooks/charmhelpers/contrib/openstack/neutron.py 2013-10-15 01:32:42 +0000 |
82 | +++ hooks/charmhelpers/contrib/openstack/neutron.py 2013-11-06 04:26:29 +0000 |
83 | @@ -34,13 +34,23 @@ |
84 | 'services': ['quantum-plugin-openvswitch-agent'], |
85 | 'packages': [[headers_package(), 'openvswitch-datapath-dkms'], |
86 | ['quantum-plugin-openvswitch-agent']], |
87 | + 'server_packages': ['quantum-server', |
88 | + 'quantum-plugin-openvswitch'], |
89 | + 'server_services': ['quantum-server'] |
90 | }, |
91 | 'nvp': { |
92 | 'config': '/etc/quantum/plugins/nicira/nvp.ini', |
93 | 'driver': 'quantum.plugins.nicira.nicira_nvp_plugin.' |
94 | 'QuantumPlugin.NvpPluginV2', |
95 | + 'contexts': [ |
96 | + context.SharedDBContext(user=config('neutron-database-user'), |
97 | + database=config('neutron-database'), |
98 | + relation_prefix='neutron')], |
99 | 'services': [], |
100 | 'packages': [], |
101 | + 'server_packages': ['quantum-server', |
102 | + 'quantum-plugin-nicira'], |
103 | + 'server_services': ['quantum-server'] |
104 | } |
105 | } |
106 | |
107 | @@ -60,13 +70,23 @@ |
108 | 'services': ['neutron-plugin-openvswitch-agent'], |
109 | 'packages': [[headers_package(), 'openvswitch-datapath-dkms'], |
110 | ['quantum-plugin-openvswitch-agent']], |
111 | + 'server_packages': ['neutron-server', |
112 | + 'neutron-plugin-openvswitch'], |
113 | + 'server_services': ['neutron-server'] |
114 | }, |
115 | 'nvp': { |
116 | 'config': '/etc/neutron/plugins/nicira/nvp.ini', |
117 | 'driver': 'neutron.plugins.nicira.nicira_nvp_plugin.' |
118 | 'NeutronPlugin.NvpPluginV2', |
119 | + 'contexts': [ |
120 | + context.SharedDBContext(user=config('neutron-database-user'), |
121 | + database=config('neutron-database'), |
122 | + relation_prefix='neutron')], |
123 | 'services': [], |
124 | 'packages': [], |
125 | + 'server_packages': ['neutron-server', |
126 | + 'neutron-plugin-nicira'], |
127 | + 'server_services': ['neutron-server'] |
128 | } |
129 | } |
130 | |
131 | |
132 | === modified file 'hooks/charmhelpers/contrib/openstack/utils.py' |
133 | --- hooks/charmhelpers/contrib/openstack/utils.py 2013-10-15 01:32:42 +0000 |
134 | +++ hooks/charmhelpers/contrib/openstack/utils.py 2013-11-06 04:26:29 +0000 |
135 | @@ -13,19 +13,28 @@ |
136 | config, |
137 | log as juju_log, |
138 | charm_dir, |
139 | -) |
140 | - |
141 | -from charmhelpers.core.host import ( |
142 | - lsb_release, |
143 | -) |
144 | - |
145 | -from charmhelpers.fetch import ( |
146 | - apt_install, |
147 | -) |
148 | + ERROR, |
149 | + INFO |
150 | +) |
151 | + |
152 | +from charmhelpers.contrib.storage.linux.lvm import ( |
153 | + deactivate_lvm_volume_group, |
154 | + is_lvm_physical_volume, |
155 | + remove_lvm_physical_volume, |
156 | +) |
157 | + |
158 | +from charmhelpers.core.host import lsb_release, mounts, umount |
159 | +from charmhelpers.fetch import apt_install |
160 | +from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk |
161 | +from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device |
162 | |
163 | CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu" |
164 | CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA' |
165 | |
166 | +DISTRO_PROPOSED = ('deb http://archive.ubuntu.com/ubuntu/ %s-proposed ' |
167 | + 'restricted main multiverse universe') |
168 | + |
169 | + |
170 | UBUNTU_OPENSTACK_RELEASE = OrderedDict([ |
171 | ('oneiric', 'diablo'), |
172 | ('precise', 'essex'), |
173 | @@ -57,6 +66,8 @@ |
174 | ('1.9.0', 'havana'), |
175 | ]) |
176 | |
177 | +DEFAULT_LOOPBACK_SIZE = '5G' |
178 | + |
179 | |
180 | def error_out(msg): |
181 | juju_log("FATAL ERROR: %s" % msg, level='ERROR') |
182 | @@ -67,7 +78,7 @@ |
183 | '''Derive OpenStack release codename from a given installation source.''' |
184 | ubuntu_rel = lsb_release()['DISTRIB_CODENAME'] |
185 | rel = '' |
186 | - if src == 'distro': |
187 | + if src in ['distro', 'distro-proposed']: |
188 | try: |
189 | rel = UBUNTU_OPENSTACK_RELEASE[ubuntu_rel] |
190 | except KeyError: |
191 | @@ -202,6 +213,10 @@ |
192 | '''Configure apt installation source.''' |
193 | if rel == 'distro': |
194 | return |
195 | + elif rel == 'distro-proposed': |
196 | + ubuntu_rel = lsb_release()['DISTRIB_CODENAME'] |
197 | + with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f: |
198 | + f.write(DISTRO_PROPOSED % ubuntu_rel) |
199 | elif rel[:4] == "ppa:": |
200 | src = rel |
201 | subprocess.check_call(["add-apt-repository", "-y", src]) |
202 | @@ -299,6 +314,62 @@ |
203 | return apt.version_compare(available_vers, cur_vers) == 1 |
204 | |
205 | |
206 | +def ensure_block_device(block_device): |
207 | + ''' |
208 | + Confirm block_device, create as loopback if necessary. |
209 | + |
210 | + :param block_device: str: Full path of block device to ensure. |
211 | + |
212 | + :returns: str: Full path of ensured block device. |
213 | + ''' |
214 | + _none = ['None', 'none', None] |
215 | + if (block_device in _none): |
216 | + error_out('prepare_storage(): Missing required input: ' |
217 | + 'block_device=%s.' % block_device, level=ERROR) |
218 | + |
219 | + if block_device.startswith('/dev/'): |
220 | + bdev = block_device |
221 | + elif block_device.startswith('/'): |
222 | + _bd = block_device.split('|') |
223 | + if len(_bd) == 2: |
224 | + bdev, size = _bd |
225 | + else: |
226 | + bdev = block_device |
227 | + size = DEFAULT_LOOPBACK_SIZE |
228 | + bdev = ensure_loopback_device(bdev, size) |
229 | + else: |
230 | + bdev = '/dev/%s' % block_device |
231 | + |
232 | + if not is_block_device(bdev): |
233 | + error_out('Failed to locate valid block device at %s' % bdev, |
234 | + level=ERROR) |
235 | + |
236 | + return bdev |
237 | + |
238 | + |
239 | +def clean_storage(block_device): |
240 | + ''' |
241 | + Ensures a block device is clean. That is: |
242 | + - unmounted |
243 | + - any lvm volume groups are deactivated |
244 | + - any lvm physical device signatures removed |
245 | + - partition table wiped |
246 | + |
247 | + :param block_device: str: Full path to block device to clean. |
248 | + ''' |
249 | + for mp, d in mounts(): |
250 | + if d == block_device: |
251 | + juju_log('clean_storage(): %s is mounted @ %s, unmounting.' % |
252 | + (d, mp), level=INFO) |
253 | + umount(mp, persist=True) |
254 | + |
255 | + if is_lvm_physical_volume(block_device): |
256 | + deactivate_lvm_volume_group(block_device) |
257 | + remove_lvm_physical_volume(block_device) |
258 | + else: |
259 | + zap_disk(block_device) |
260 | + |
261 | + |
262 | def is_ip(address): |
263 | """ |
264 | Returns True if address is a valid IP address. |
265 | |
266 | === modified file 'hooks/charmhelpers/contrib/storage/linux/ceph.py' |
267 | --- hooks/charmhelpers/contrib/storage/linux/ceph.py 2013-09-24 14:54:12 +0000 |
268 | +++ hooks/charmhelpers/contrib/storage/linux/ceph.py 2013-11-06 04:26:29 +0000 |
269 | @@ -102,8 +102,12 @@ |
270 | Return a list of all Ceph Object Storage Daemons |
271 | currently in the cluster |
272 | ''' |
273 | - return json.loads(check_output(['ceph', '--id', service, |
274 | - 'osd', 'ls', '--format=json'])) |
275 | + version = ceph_version() |
276 | + if version and version >= '0.56': |
277 | + return json.loads(check_output(['ceph', '--id', service, |
278 | + 'osd', 'ls', '--format=json'])) |
279 | + else: |
280 | + return None |
281 | |
282 | |
283 | def create_pool(service, name, replicas=2): |
284 | @@ -114,7 +118,13 @@ |
285 | return |
286 | # Calculate the number of placement groups based |
287 | # on upstream recommended best practices. |
288 | - pgnum = (len(get_osds(service)) * 100 / replicas) |
289 | + osds = get_osds(service) |
290 | + if osds: |
291 | + pgnum = (len(osds) * 100 / replicas) |
292 | + else: |
293 | + # NOTE(james-page): Default to 200 for older ceph versions |
294 | + # which don't support OSD query from cli |
295 | + pgnum = 200 |
296 | cmd = [ |
297 | 'ceph', '--id', service, |
298 | 'osd', 'pool', 'create', |
299 | @@ -357,3 +367,17 @@ |
300 | if user and group: |
301 | check_call(['chown', '%s.%s' % (user, group), keyring]) |
302 | return True |
303 | + |
304 | + |
305 | +def ceph_version(): |
306 | + ''' Retrieve the local version of ceph ''' |
307 | + if os.path.exists('/usr/bin/ceph'): |
308 | + cmd = ['ceph', '-v'] |
309 | + output = check_output(cmd) |
310 | + output = output.split() |
311 | + if len(output) > 3: |
312 | + return output[2] |
313 | + else: |
314 | + return None |
315 | + else: |
316 | + return None |
317 | |
318 | === modified file 'hooks/charmhelpers/core/hookenv.py' |
319 | --- hooks/charmhelpers/core/hookenv.py 2013-08-14 22:31:44 +0000 |
320 | +++ hooks/charmhelpers/core/hookenv.py 2013-11-06 04:26:29 +0000 |
321 | @@ -9,6 +9,7 @@ |
322 | import yaml |
323 | import subprocess |
324 | import UserDict |
325 | +from subprocess import CalledProcessError |
326 | |
327 | CRITICAL = "CRITICAL" |
328 | ERROR = "ERROR" |
329 | @@ -21,7 +22,7 @@ |
330 | |
331 | |
332 | def cached(func): |
333 | - ''' Cache return values for multiple executions of func + args |
334 | + """Cache return values for multiple executions of func + args |
335 | |
336 | For example: |
337 | |
338 | @@ -32,7 +33,7 @@ |
339 | unit_get('test') |
340 | |
341 | will cache the result of unit_get + 'test' for future calls. |
342 | - ''' |
343 | + """ |
344 | def wrapper(*args, **kwargs): |
345 | global cache |
346 | key = str((func, args, kwargs)) |
347 | @@ -46,8 +47,8 @@ |
348 | |
349 | |
350 | def flush(key): |
351 | - ''' Flushes any entries from function cache where the |
352 | - key is found in the function+args ''' |
353 | + """Flushes any entries from function cache where the |
354 | + key is found in the function+args """ |
355 | flush_list = [] |
356 | for item in cache: |
357 | if key in item: |
358 | @@ -57,7 +58,7 @@ |
359 | |
360 | |
361 | def log(message, level=None): |
362 | - "Write a message to the juju log" |
363 | + """Write a message to the juju log""" |
364 | command = ['juju-log'] |
365 | if level: |
366 | command += ['-l', level] |
367 | @@ -66,7 +67,7 @@ |
368 | |
369 | |
370 | class Serializable(UserDict.IterableUserDict): |
371 | - "Wrapper, an object that can be serialized to yaml or json" |
372 | + """Wrapper, an object that can be serialized to yaml or json""" |
373 | |
374 | def __init__(self, obj): |
375 | # wrap the object |
376 | @@ -96,11 +97,11 @@ |
377 | self.data = state |
378 | |
379 | def json(self): |
380 | - "Serialize the object to json" |
381 | + """Serialize the object to json""" |
382 | return json.dumps(self.data) |
383 | |
384 | def yaml(self): |
385 | - "Serialize the object to yaml" |
386 | + """Serialize the object to yaml""" |
387 | return yaml.dump(self.data) |
388 | |
389 | |
390 | @@ -119,38 +120,38 @@ |
391 | |
392 | |
393 | def in_relation_hook(): |
394 | - "Determine whether we're running in a relation hook" |
395 | + """Determine whether we're running in a relation hook""" |
396 | return 'JUJU_RELATION' in os.environ |
397 | |
398 | |
399 | def relation_type(): |
400 | - "The scope for the current relation hook" |
401 | + """The scope for the current relation hook""" |
402 | return os.environ.get('JUJU_RELATION', None) |
403 | |
404 | |
405 | def relation_id(): |
406 | - "The relation ID for the current relation hook" |
407 | + """The relation ID for the current relation hook""" |
408 | return os.environ.get('JUJU_RELATION_ID', None) |
409 | |
410 | |
411 | def local_unit(): |
412 | - "Local unit ID" |
413 | + """Local unit ID""" |
414 | return os.environ['JUJU_UNIT_NAME'] |
415 | |
416 | |
417 | def remote_unit(): |
418 | - "The remote unit for the current relation hook" |
419 | + """The remote unit for the current relation hook""" |
420 | return os.environ['JUJU_REMOTE_UNIT'] |
421 | |
422 | |
423 | def service_name(): |
424 | - "The name service group this unit belongs to" |
425 | + """The name service group this unit belongs to""" |
426 | return local_unit().split('/')[0] |
427 | |
428 | |
429 | @cached |
430 | def config(scope=None): |
431 | - "Juju charm configuration" |
432 | + """Juju charm configuration""" |
433 | config_cmd_line = ['config-get'] |
434 | if scope is not None: |
435 | config_cmd_line.append(scope) |
436 | @@ -163,6 +164,7 @@ |
437 | |
438 | @cached |
439 | def relation_get(attribute=None, unit=None, rid=None): |
440 | + """Get relation information""" |
441 | _args = ['relation-get', '--format=json'] |
442 | if rid: |
443 | _args.append('-r') |
444 | @@ -174,9 +176,14 @@ |
445 | return json.loads(subprocess.check_output(_args)) |
446 | except ValueError: |
447 | return None |
448 | + except CalledProcessError, e: |
449 | + if e.returncode == 2: |
450 | + return None |
451 | + raise |
452 | |
453 | |
454 | def relation_set(relation_id=None, relation_settings={}, **kwargs): |
455 | + """Set relation information for the current unit""" |
456 | relation_cmd_line = ['relation-set'] |
457 | if relation_id is not None: |
458 | relation_cmd_line.extend(('-r', relation_id)) |
459 | @@ -192,7 +199,7 @@ |
460 | |
461 | @cached |
462 | def relation_ids(reltype=None): |
463 | - "A list of relation_ids" |
464 | + """A list of relation_ids""" |
465 | reltype = reltype or relation_type() |
466 | relid_cmd_line = ['relation-ids', '--format=json'] |
467 | if reltype is not None: |
468 | @@ -203,7 +210,7 @@ |
469 | |
470 | @cached |
471 | def related_units(relid=None): |
472 | - "A list of related units" |
473 | + """A list of related units""" |
474 | relid = relid or relation_id() |
475 | units_cmd_line = ['relation-list', '--format=json'] |
476 | if relid is not None: |
477 | @@ -213,7 +220,7 @@ |
478 | |
479 | @cached |
480 | def relation_for_unit(unit=None, rid=None): |
481 | - "Get the json represenation of a unit's relation" |
482 | + """Get the json represenation of a unit's relation""" |
483 | unit = unit or remote_unit() |
484 | relation = relation_get(unit=unit, rid=rid) |
485 | for key in relation: |
486 | @@ -225,7 +232,7 @@ |
487 | |
488 | @cached |
489 | def relations_for_id(relid=None): |
490 | - "Get relations of a specific relation ID" |
491 | + """Get relations of a specific relation ID""" |
492 | relation_data = [] |
493 | relid = relid or relation_ids() |
494 | for unit in related_units(relid): |
495 | @@ -237,7 +244,7 @@ |
496 | |
497 | @cached |
498 | def relations_of_type(reltype=None): |
499 | - "Get relations of a specific type" |
500 | + """Get relations of a specific type""" |
501 | relation_data = [] |
502 | reltype = reltype or relation_type() |
503 | for relid in relation_ids(reltype): |
504 | @@ -249,7 +256,7 @@ |
505 | |
506 | @cached |
507 | def relation_types(): |
508 | - "Get a list of relation types supported by this charm" |
509 | + """Get a list of relation types supported by this charm""" |
510 | charmdir = os.environ.get('CHARM_DIR', '') |
511 | mdf = open(os.path.join(charmdir, 'metadata.yaml')) |
512 | md = yaml.safe_load(mdf) |
513 | @@ -264,6 +271,7 @@ |
514 | |
515 | @cached |
516 | def relations(): |
517 | + """Get a nested dictionary of relation data for all related units""" |
518 | rels = {} |
519 | for reltype in relation_types(): |
520 | relids = {} |
521 | @@ -277,15 +285,35 @@ |
522 | return rels |
523 | |
524 | |
525 | +@cached |
526 | +def is_relation_made(relation, keys='private-address'): |
527 | + ''' |
528 | + Determine whether a relation is established by checking for |
529 | + presence of key(s). If a list of keys is provided, they |
530 | + must all be present for the relation to be identified as made |
531 | + ''' |
532 | + if isinstance(keys, str): |
533 | + keys = [keys] |
534 | + for r_id in relation_ids(relation): |
535 | + for unit in related_units(r_id): |
536 | + context = {} |
537 | + for k in keys: |
538 | + context[k] = relation_get(k, rid=r_id, |
539 | + unit=unit) |
540 | + if None not in context.values(): |
541 | + return True |
542 | + return False |
543 | + |
544 | + |
545 | def open_port(port, protocol="TCP"): |
546 | - "Open a service network port" |
547 | + """Open a service network port""" |
548 | _args = ['open-port'] |
549 | _args.append('{}/{}'.format(port, protocol)) |
550 | subprocess.check_call(_args) |
551 | |
552 | |
553 | def close_port(port, protocol="TCP"): |
554 | - "Close a service network port" |
555 | + """Close a service network port""" |
556 | _args = ['close-port'] |
557 | _args.append('{}/{}'.format(port, protocol)) |
558 | subprocess.check_call(_args) |
559 | @@ -293,6 +321,7 @@ |
560 | |
561 | @cached |
562 | def unit_get(attribute): |
563 | + """Get the unit ID for the remote unit""" |
564 | _args = ['unit-get', '--format=json', attribute] |
565 | try: |
566 | return json.loads(subprocess.check_output(_args)) |
567 | @@ -301,22 +330,46 @@ |
568 | |
569 | |
570 | def unit_private_ip(): |
571 | + """Get this unit's private IP address""" |
572 | return unit_get('private-address') |
573 | |
574 | |
575 | class UnregisteredHookError(Exception): |
576 | + """Raised when an undefined hook is called""" |
577 | pass |
578 | |
579 | |
580 | class Hooks(object): |
581 | + """A convenient handler for hook functions. |
582 | + |
583 | + Example: |
584 | + hooks = Hooks() |
585 | + |
586 | + # register a hook, taking its name from the function name |
587 | + @hooks.hook() |
588 | + def install(): |
589 | + ... |
590 | + |
591 | + # register a hook, providing a custom hook name |
592 | + @hooks.hook("config-changed") |
593 | + def config_changed(): |
594 | + ... |
595 | + |
596 | + if __name__ == "__main__": |
597 | + # execute a hook based on the name the program is called by |
598 | + hooks.execute(sys.argv) |
599 | + """ |
600 | + |
601 | def __init__(self): |
602 | super(Hooks, self).__init__() |
603 | self._hooks = {} |
604 | |
605 | def register(self, name, function): |
606 | + """Register a hook""" |
607 | self._hooks[name] = function |
608 | |
609 | def execute(self, args): |
610 | + """Execute a registered hook based on args[0]""" |
611 | hook_name = os.path.basename(args[0]) |
612 | if hook_name in self._hooks: |
613 | self._hooks[hook_name]() |
614 | @@ -324,6 +377,7 @@ |
615 | raise UnregisteredHookError(hook_name) |
616 | |
617 | def hook(self, *hook_names): |
618 | + """Decorator, registering them as hooks""" |
619 | def wrapper(decorated): |
620 | for hook_name in hook_names: |
621 | self.register(hook_name, decorated) |
622 | @@ -337,4 +391,5 @@ |
623 | |
624 | |
625 | def charm_dir(): |
626 | + """Return the root directory of the current charm""" |
627 | return os.environ.get('CHARM_DIR') |
628 | |
629 | === modified file 'hooks/charmhelpers/core/host.py' |
630 | --- hooks/charmhelpers/core/host.py 2013-09-20 15:52:45 +0000 |
631 | +++ hooks/charmhelpers/core/host.py 2013-11-06 04:26:29 +0000 |
632 | @@ -19,18 +19,22 @@ |
633 | |
634 | |
635 | def service_start(service_name): |
636 | + """Start a system service""" |
637 | return service('start', service_name) |
638 | |
639 | |
640 | def service_stop(service_name): |
641 | + """Stop a system service""" |
642 | return service('stop', service_name) |
643 | |
644 | |
645 | def service_restart(service_name): |
646 | + """Restart a system service""" |
647 | return service('restart', service_name) |
648 | |
649 | |
650 | def service_reload(service_name, restart_on_failure=False): |
651 | + """Reload a system service, optionally falling back to restart if reload fails""" |
652 | service_result = service('reload', service_name) |
653 | if not service_result and restart_on_failure: |
654 | service_result = service('restart', service_name) |
655 | @@ -38,11 +42,13 @@ |
656 | |
657 | |
658 | def service(action, service_name): |
659 | + """Control a system service""" |
660 | cmd = ['service', service_name, action] |
661 | return subprocess.call(cmd) == 0 |
662 | |
663 | |
664 | def service_running(service): |
665 | + """Determine whether a system service is running""" |
666 | try: |
667 | output = subprocess.check_output(['service', service, 'status']) |
668 | except subprocess.CalledProcessError: |
669 | @@ -55,7 +61,7 @@ |
670 | |
671 | |
672 | def adduser(username, password=None, shell='/bin/bash', system_user=False): |
673 | - """Add a user""" |
674 | + """Add a user to the system""" |
675 | try: |
676 | user_info = pwd.getpwnam(username) |
677 | log('user {0} already exists!'.format(username)) |
678 | @@ -138,7 +144,7 @@ |
679 | |
680 | |
681 | def mount(device, mountpoint, options=None, persist=False): |
682 | - '''Mount a filesystem''' |
683 | + """Mount a filesystem at a particular mountpoint""" |
684 | cmd_args = ['mount'] |
685 | if options is not None: |
686 | cmd_args.extend(['-o', options]) |
687 | @@ -155,7 +161,7 @@ |
688 | |
689 | |
690 | def umount(mountpoint, persist=False): |
691 | - '''Unmount a filesystem''' |
692 | + """Unmount a filesystem""" |
693 | cmd_args = ['umount', mountpoint] |
694 | try: |
695 | subprocess.check_output(cmd_args) |
696 | @@ -169,7 +175,7 @@ |
697 | |
698 | |
699 | def mounts(): |
700 | - '''List of all mounted volumes as [[mountpoint,device],[...]]''' |
701 | + """Get a list of all mounted volumes as [[mountpoint,device],[...]]""" |
702 | with open('/proc/mounts') as f: |
703 | # [['/mount/point','/dev/path'],[...]] |
704 | system_mounts = [m[1::-1] for m in [l.strip().split() |
705 | @@ -178,7 +184,7 @@ |
706 | |
707 | |
708 | def file_hash(path): |
709 | - ''' Generate a md5 hash of the contents of 'path' or None if not found ''' |
710 | + """Generate a md5 hash of the contents of 'path' or None if not found """ |
711 | if os.path.exists(path): |
712 | h = hashlib.md5() |
713 | with open(path, 'r') as source: |
714 | @@ -189,7 +195,7 @@ |
715 | |
716 | |
717 | def restart_on_change(restart_map): |
718 | - ''' Restart services based on configuration files changing |
719 | + """Restart services based on configuration files changing |
720 | |
721 | This function is used a decorator, for example |
722 | |
723 | @@ -202,7 +208,7 @@ |
724 | In this example, the cinder-api and cinder-volume services |
725 | would be restarted if /etc/ceph/ceph.conf is changed by the |
726 | ceph_client_changed function. |
727 | - ''' |
728 | + """ |
729 | def wrap(f): |
730 | def wrapped_f(*args): |
731 | checksums = {} |
732 | @@ -220,7 +226,7 @@ |
733 | |
734 | |
735 | def lsb_release(): |
736 | - '''Return /etc/lsb-release in a dict''' |
737 | + """Return /etc/lsb-release in a dict""" |
738 | d = {} |
739 | with open('/etc/lsb-release', 'r') as lsb: |
740 | for l in lsb: |
741 | @@ -230,7 +236,7 @@ |
742 | |
743 | |
744 | def pwgen(length=None): |
745 | - '''Generate a random pasword.''' |
746 | + """Generate a random pasword.""" |
747 | if length is None: |
748 | length = random.choice(range(35, 45)) |
749 | alphanumeric_chars = [ |
750 | |
751 | === modified file 'hooks/charmhelpers/fetch/__init__.py' |
752 | --- hooks/charmhelpers/fetch/__init__.py 2013-09-23 13:22:13 +0000 |
753 | +++ hooks/charmhelpers/fetch/__init__.py 2013-11-06 04:26:29 +0000 |
754 | @@ -20,6 +20,32 @@ |
755 | PROPOSED_POCKET = """# Proposed |
756 | deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted |
757 | """ |
758 | +CLOUD_ARCHIVE_POCKETS = { |
759 | + # Folsom |
760 | + 'folsom': 'precise-updates/folsom', |
761 | + 'precise-folsom': 'precise-updates/folsom', |
762 | + 'precise-folsom/updates': 'precise-updates/folsom', |
763 | + 'precise-updates/folsom': 'precise-updates/folsom', |
764 | + 'folsom/proposed': 'precise-proposed/folsom', |
765 | + 'precise-folsom/proposed': 'precise-proposed/folsom', |
766 | + 'precise-proposed/folsom': 'precise-proposed/folsom', |
767 | + # Grizzly |
768 | + 'grizzly': 'precise-updates/grizzly', |
769 | + 'precise-grizzly': 'precise-updates/grizzly', |
770 | + 'precise-grizzly/updates': 'precise-updates/grizzly', |
771 | + 'precise-updates/grizzly': 'precise-updates/grizzly', |
772 | + 'grizzly/proposed': 'precise-proposed/grizzly', |
773 | + 'precise-grizzly/proposed': 'precise-proposed/grizzly', |
774 | + 'precise-proposed/grizzly': 'precise-proposed/grizzly', |
775 | + # Havana |
776 | + 'havana': 'precise-updates/havana', |
777 | + 'precise-havana': 'precise-updates/havana', |
778 | + 'precise-havana/updates': 'precise-updates/havana', |
779 | + 'precise-updates/havana': 'precise-updates/havana', |
780 | + 'havana/proposed': 'precise-proposed/havana', |
781 | + 'precies-havana/proposed': 'precise-proposed/havana', |
782 | + 'precise-proposed/havana': 'precise-proposed/havana', |
783 | +} |
784 | |
785 | |
786 | def filter_installed_packages(packages): |
787 | @@ -79,16 +105,35 @@ |
788 | subprocess.call(cmd) |
789 | |
790 | |
791 | +def apt_hold(packages, fatal=False): |
792 | + """Hold one or more packages""" |
793 | + cmd = ['apt-mark', 'hold'] |
794 | + if isinstance(packages, basestring): |
795 | + cmd.append(packages) |
796 | + else: |
797 | + cmd.extend(packages) |
798 | + log("Holding {}".format(packages)) |
799 | + if fatal: |
800 | + subprocess.check_call(cmd) |
801 | + else: |
802 | + subprocess.call(cmd) |
803 | + |
804 | + |
805 | def add_source(source, key=None): |
806 | - if ((source.startswith('ppa:') or |
807 | - source.startswith('http:'))): |
808 | + if (source.startswith('ppa:') or |
809 | + source.startswith('http:') or |
810 | + source.startswith('deb ') or |
811 | + source.startswith('cloud-archive:')): |
812 | subprocess.check_call(['add-apt-repository', '--yes', source]) |
813 | elif source.startswith('cloud:'): |
814 | apt_install(filter_installed_packages(['ubuntu-cloud-keyring']), |
815 | fatal=True) |
816 | pocket = source.split(':')[-1] |
817 | + if pocket not in CLOUD_ARCHIVE_POCKETS: |
818 | + raise SourceConfigError('Unsupported cloud: source option %s' % pocket) |
819 | + actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket] |
820 | with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt: |
821 | - apt.write(CLOUD_ARCHIVE.format(pocket)) |
822 | + apt.write(CLOUD_ARCHIVE.format(actual_pocket)) |
823 | elif source == 'proposed': |
824 | release = lsb_release()['DISTRIB_CODENAME'] |
825 | with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt: |
826 | @@ -118,8 +163,11 @@ |
827 | Note that 'null' (a.k.a. None) should not be quoted. |
828 | """ |
829 | sources = safe_load(config(sources_var)) |
830 | - keys = safe_load(config(keys_var)) |
831 | - if isinstance(sources, basestring) and isinstance(keys, basestring): |
832 | + keys = config(keys_var) |
833 | + if keys is not None: |
834 | + keys = safe_load(keys) |
835 | + if isinstance(sources, basestring) and ( |
836 | + keys is None or isinstance(keys, basestring)): |
837 | add_source(sources, keys) |
838 | else: |
839 | if not len(sources) == len(keys): |
840 | |
841 | === modified file 'hooks/charmhelpers/fetch/bzrurl.py' |
842 | --- hooks/charmhelpers/fetch/bzrurl.py 2013-09-23 13:22:13 +0000 |
843 | +++ hooks/charmhelpers/fetch/bzrurl.py 2013-11-06 04:26:29 +0000 |
844 | @@ -12,6 +12,7 @@ |
845 | apt_install("python-bzrlib") |
846 | from bzrlib.branch import Branch |
847 | |
848 | + |
849 | class BzrUrlFetchHandler(BaseFetchHandler): |
850 | """Handler for bazaar branches via generic and lp URLs""" |
851 | def can_handle(self, source): |
852 | @@ -46,4 +47,3 @@ |
853 | except OSError as e: |
854 | raise UnhandledSource(e.strerror) |
855 | return dest_dir |
856 | - |
857 | |
858 | === modified file 'hooks/glance_contexts.py' |
859 | --- hooks/glance_contexts.py 2013-10-19 21:01:07 +0000 |
860 | +++ hooks/glance_contexts.py 2013-11-06 04:26:29 +0000 |
861 | @@ -1,4 +1,5 @@ |
862 | from charmhelpers.core.hookenv import ( |
863 | + is_relation_made, |
864 | relation_ids, |
865 | related_units, |
866 | relation_get, |
867 | @@ -16,16 +17,6 @@ |
868 | ) |
869 | |
870 | |
871 | -# TODO: switch to charmhelpers once landed |
872 | -# NOTE: zero units tests - done in charmhelpers |
873 | -def is_relation_made(relation, key='private-address'): |
874 | - for r_id in relation_ids(relation): |
875 | - for unit in related_units(r_id): |
876 | - if relation_get(key, rid=r_id, unit=unit): |
877 | - return True |
878 | - return False |
879 | - |
880 | - |
881 | class CephGlanceContext(OSContextGenerator): |
882 | interfaces = ['ceph-glance'] |
883 | |
884 | |
885 | === modified file 'hooks/glance_relations.py' |
886 | --- hooks/glance_relations.py 2013-10-19 21:01:07 +0000 |
887 | +++ hooks/glance_relations.py 2013-11-06 04:26:29 +0000 |
888 | @@ -313,8 +313,22 @@ |
889 | image_service_joined(relation_id=r_id) |
890 | |
891 | |
892 | +@hooks.hook('amqp-relation-joined') |
893 | +def amqp_joined(): |
894 | + conf = config() |
895 | + relation_set(username=conf['rabbit-user'], vhost=conf['rabbit-vhost']) |
896 | + |
897 | + |
898 | +@hooks.hook('amqp-relation-changed') |
899 | +@restart_on_change(restart_map()) |
900 | +def amqp_changed(): |
901 | + if 'amqp' not in CONFIGS.complete_contexts(): |
902 | + juju_log('amqp relation incomplete. Peer not ready?') |
903 | + return |
904 | + CONFIGS.write(GLANCE_API_CONF) |
905 | + |
906 | if __name__ == '__main__': |
907 | try: |
908 | hooks.execute(sys.argv) |
909 | except UnregisteredHookError as e: |
910 | - juju_log('Unknown hook {} - skiping.'.format(e)) |
911 | + juju_log('Unknown hook {} - skipping.'.format(e)) |
912 | |
913 | === modified file 'hooks/glance_utils.py' |
914 | --- hooks/glance_utils.py 2013-10-19 21:01:07 +0000 |
915 | +++ hooks/glance_utils.py 2013-11-06 04:26:29 +0000 |
916 | @@ -68,6 +68,7 @@ |
917 | }), |
918 | (GLANCE_API_CONF, { |
919 | 'hook_contexts': [context.SharedDBContext(), |
920 | + context.AMQPContext(), |
921 | context.IdentityServiceContext(), |
922 | glance_contexts.CephGlanceContext(), |
923 | glance_contexts.ObjectStoreContext(), |
924 | |
925 | === modified file 'metadata.yaml' |
926 | --- metadata.yaml 2013-09-26 09:47:58 +0000 |
927 | +++ metadata.yaml 2013-11-06 04:26:29 +0000 |
928 | @@ -14,6 +14,8 @@ |
929 | requires: |
930 | shared-db: |
931 | interface: mysql-shared |
932 | + amqp: |
933 | + interface: rabbitmq |
934 | object-store: |
935 | interface: swift-proxy |
936 | identity-service: |
937 | |
938 | === modified file 'templates/folsom/glance-api.conf' |
939 | --- templates/folsom/glance-api.conf 2013-10-07 14:48:52 +0000 |
940 | +++ templates/folsom/glance-api.conf 2013-11-06 04:26:29 +0000 |
941 | @@ -29,31 +29,15 @@ |
942 | registry_host = 0.0.0.0 |
943 | registry_port = 9191 |
944 | registry_client_protocol = http |
945 | -notifier_strategy = noop |
946 | -rabbit_host = localhost |
947 | -rabbit_port = 5672 |
948 | -rabbit_use_ssl = false |
949 | -rabbit_userid = guest |
950 | -rabbit_password = guest |
951 | -rabbit_virtual_host = / |
952 | -rabbit_notification_exchange = glance |
953 | -rabbit_notification_topic = glance_notifications |
954 | -rabbit_durable_queues = False |
955 | -qpid_notification_exchange = glance |
956 | -qpid_notification_topic = glance_notifications |
957 | -qpid_host = localhost |
958 | -qpid_port = 5672 |
959 | -qpid_username = |
960 | -qpid_password = |
961 | -qpid_sasl_mechanisms = |
962 | -qpid_reconnect_timeout = 0 |
963 | -qpid_reconnect_limit = 0 |
964 | -qpid_reconnect_interval_min = 0 |
965 | -qpid_reconnect_interval_max = 0 |
966 | -qpid_reconnect_interval = 0 |
967 | -qpid_heartbeat = 5 |
968 | -qpid_protocol = tcp |
969 | -qpid_tcp_nodelay = True |
970 | + |
971 | +{% if rabbitmq_host -%} |
972 | +notifier_strategy = rabbit |
973 | +rabbit_host = {{ rabbitmq_host }} |
974 | +rabbit_userid = {{ rabbitmq_user }} |
975 | +rabbit_password = {{ rabbitmq_password }} |
976 | +rabbit_virtual_host = {{ rabbitmq_virtual_host }} |
977 | +{% endif -%} |
978 | + |
979 | filesystem_store_datadir = /var/lib/glance/images/ |
980 | |
981 | {% if swift_store %} |
982 | |
983 | === modified file 'unit_tests/test_glance_relations.py' |
984 | --- unit_tests/test_glance_relations.py 2013-10-20 18:35:42 +0000 |
985 | +++ unit_tests/test_glance_relations.py 2013-11-06 04:26:29 +0000 |
986 | @@ -370,7 +370,78 @@ |
987 | def test_ha_relation_changed_not_clustered(self): |
988 | self.relation_get.return_value = False |
989 | relations.ha_relation_changed() |
990 | - self.assertTrue(self.juju_log.called) |
991 | + self.juju_log.assert_called_with( |
992 | + 'ha_changed: hacluster subordinate is not fully clustered.' |
993 | + ) |
994 | + |
995 | + @patch.object(relations, 'keystone_joined') |
996 | + @patch.object(relations, 'CONFIGS') |
997 | + def test_configure_https_enable_with_identity_service(self, configs, keystone_joined): |
998 | + configs.complete_contexts = MagicMock() |
999 | + configs.complete_contexts.return_value = ['https'] |
1000 | + configs.write = MagicMock() |
1001 | + self.relation_ids.return_value = ['identity-service:0'] |
1002 | + relations.configure_https() |
1003 | + cmd = ['a2ensite', 'openstack_https_frontend'] |
1004 | + self.check_call.assert_called_with(cmd) |
1005 | + keystone_joined.assert_called_with(relation_id='identity-service:0') |
1006 | + |
1007 | + @patch.object(relations, 'keystone_joined') |
1008 | + @patch.object(relations, 'CONFIGS') |
1009 | + def test_configure_https_disable_with_keystone_joined(self, configs, keystone_joined): |
1010 | + configs.complete_contexts = MagicMock() |
1011 | + configs.complete_contexts.return_value = [''] |
1012 | + configs.write = MagicMock() |
1013 | + self.relation_ids.return_value = ['identity-service:0'] |
1014 | + relations.configure_https() |
1015 | + cmd = ['a2dissite', 'openstack_https_frontend'] |
1016 | + self.check_call.assert_called_with(cmd) |
1017 | + keystone_joined.assert_called_with(relation_id='identity-service:0') |
1018 | + |
1019 | + @patch.object(relations, 'image_service_joined') |
1020 | + @patch.object(relations, 'CONFIGS') |
1021 | + def test_configure_https_enable_with_image_service(self, configs, image_service_joined): |
1022 | + configs.complete_contexts = MagicMock() |
1023 | + configs.complete_contexts.return_value = ['https'] |
1024 | + configs.write = MagicMock() |
1025 | + self.relation_ids.return_value = ['image-service:0'] |
1026 | + relations.configure_https() |
1027 | + cmd = ['a2ensite', 'openstack_https_frontend'] |
1028 | + self.check_call.assert_called_with(cmd) |
1029 | + image_service_joined.assert_called_with(relation_id='image-service:0') |
1030 | + |
1031 | + @patch.object(relations, 'image_service_joined') |
1032 | + @patch.object(relations, 'CONFIGS') |
1033 | + def test_configure_https_disable_with_image_service(self, configs, image_service_joined): |
1034 | + configs.complete_contexts = MagicMock() |
1035 | + configs.complete_contexts.return_value = [''] |
1036 | + configs.write = MagicMock() |
1037 | + self.relation_ids.return_value = ['image-service:0'] |
1038 | + relations.configure_https() |
1039 | + cmd = ['a2dissite', 'openstack_https_frontend'] |
1040 | + self.check_call.assert_called_with(cmd) |
1041 | + image_service_joined.assert_called_with(relation_id='image-service:0') |
1042 | + |
1043 | + def test_amqp_joined(self): |
1044 | + relations.amqp_joined() |
1045 | + self.relation_set.assert_called_with(username='glance', vhost='openstack') |
1046 | + |
1047 | + @patch.object(relations, 'CONFIGS') |
1048 | + def test_amqp_changed_missing_relation_data(self, configs): |
1049 | + configs.complete_contexts = MagicMock() |
1050 | + configs.complete_contexts.return_value = [] |
1051 | + relations.amqp_changed() |
1052 | + self.juju_log.assert_called() |
1053 | + |
1054 | + @patch.object(relations, 'CONFIGS') |
1055 | + def test_amqp_changed_relation_data(self, configs): |
1056 | + configs.complete_contexts = MagicMock() |
1057 | + configs.complete_contexts.return_value = ['amqp'] |
1058 | + configs.write = MagicMock() |
1059 | + relations.amqp_changed() |
1060 | + self.assertEquals([call('/etc/glance/glance-api.conf')], |
1061 | + configs.write.call_args_list) |
1062 | + self.assertFalse(self.juju_log.called) |
1063 | |
1064 | @patch.object(relations, 'keystone_joined') |
1065 | def test_ha_relation_changed_not_leader(self, joined): |