Merge lp:~openstack-charmers/charm-helpers/to_upstream into lp:charm-helpers
- to_upstream
- Merge into devel
Proposed by
James Page
Status: | Merged |
---|---|
Approved by: | Adam Gandelman |
Approved revision: | 112 |
Merged at revision: | 79 |
Proposed branch: | lp:~openstack-charmers/charm-helpers/to_upstream |
Merge into: | lp:charm-helpers |
Diff against target: |
1400 lines (+945/-59) 10 files modified
charmhelpers/contrib/hahelpers/cluster.py (+4/-2) charmhelpers/contrib/openstack/context.py (+254/-26) charmhelpers/contrib/openstack/neutron.py (+117/-0) charmhelpers/contrib/openstack/templating.py (+23/-4) charmhelpers/contrib/openstack/utils.py (+102/-13) charmhelpers/contrib/storage/linux/ceph.py (+22/-0) tests/contrib/openstack/test_openstack_utils.py (+80/-2) tests/contrib/openstack/test_os_contexts.py (+257/-12) tests/contrib/openstack/test_os_templating.py (+46/-0) tests/contrib/storage/test_linux_ceph.py (+40/-0) |
To merge this branch: | bzr merge lp:~openstack-charmers/charm-helpers/to_upstream |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Charm Helper Maintainers | Pending | ||
Review via email:
|
Commit message
Description of the change
Bulk update for all openstack python redux charm-helper changes.
To post a comment you must log in.
- 109. By James Page
-
Update for swift havana release 1.10.0
- 110. By Adam Gandelman
-
neutron: Ensure correct headers for running kernel are installed.
- 111. By Adam Gandelman
-
Reorder list for neutron package.
- 112. By Adam Gandelman
-
Adds new SubordinateConf
igContexts, updates tests.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'charmhelpers/contrib/hahelpers/cluster.py' |
2 | --- charmhelpers/contrib/hahelpers/cluster.py 2013-07-23 11:50:28 +0000 |
3 | +++ charmhelpers/contrib/hahelpers/cluster.py 2013-10-15 01:15:14 +0000 |
4 | @@ -97,12 +97,14 @@ |
5 | return True |
6 | for r_id in relation_ids('identity-service'): |
7 | for unit in relation_list(r_id): |
8 | - if None not in [ |
9 | + rel_state = [ |
10 | relation_get('https_keystone', rid=r_id, unit=unit), |
11 | relation_get('ssl_cert', rid=r_id, unit=unit), |
12 | relation_get('ssl_key', rid=r_id, unit=unit), |
13 | relation_get('ca_cert', rid=r_id, unit=unit), |
14 | - ]: |
15 | + ] |
16 | + # NOTE: works around (LP: #1203241) |
17 | + if (None not in rel_state) and ('' not in rel_state): |
18 | return True |
19 | return False |
20 | |
21 | |
22 | === modified file 'charmhelpers/contrib/openstack/context.py' |
23 | --- charmhelpers/contrib/openstack/context.py 2013-07-19 23:29:59 +0000 |
24 | +++ charmhelpers/contrib/openstack/context.py 2013-10-15 01:15:14 +0000 |
25 | @@ -1,3 +1,4 @@ |
26 | +import json |
27 | import os |
28 | |
29 | from base64 import b64decode |
30 | @@ -6,6 +7,12 @@ |
31 | check_call |
32 | ) |
33 | |
34 | + |
35 | +from charmhelpers.fetch import ( |
36 | + apt_install, |
37 | + filter_installed_packages, |
38 | +) |
39 | + |
40 | from charmhelpers.core.hookenv import ( |
41 | config, |
42 | local_unit, |
43 | @@ -14,6 +21,9 @@ |
44 | relation_ids, |
45 | related_units, |
46 | unit_get, |
47 | + unit_private_ip, |
48 | + ERROR, |
49 | + WARNING, |
50 | ) |
51 | |
52 | from charmhelpers.contrib.hahelpers.cluster import ( |
53 | @@ -29,6 +39,10 @@ |
54 | get_ca_cert, |
55 | ) |
56 | |
57 | +from charmhelpers.contrib.openstack.neutron import ( |
58 | + neutron_plugin_attribute, |
59 | +) |
60 | + |
61 | CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt' |
62 | |
63 | |
64 | @@ -36,6 +50,13 @@ |
65 | pass |
66 | |
67 | |
68 | +def ensure_packages(packages): |
69 | + '''Install but do not upgrade required plugin packages''' |
70 | + required = filter_installed_packages(packages) |
71 | + if required: |
72 | + apt_install(required, fatal=True) |
73 | + |
74 | + |
75 | def context_complete(ctxt): |
76 | _missing = [] |
77 | for k, v in ctxt.iteritems(): |
78 | @@ -57,30 +78,43 @@ |
79 | class SharedDBContext(OSContextGenerator): |
80 | interfaces = ['shared-db'] |
81 | |
82 | + def __init__(self, database=None, user=None, relation_prefix=None): |
83 | + ''' |
84 | + Allows inspecting relation for settings prefixed with relation_prefix. |
85 | + This is useful for parsing access for multiple databases returned via |
86 | + the shared-db interface (eg, nova_password, quantum_password) |
87 | + ''' |
88 | + self.relation_prefix = relation_prefix |
89 | + self.database = database |
90 | + self.user = user |
91 | + |
92 | def __call__(self): |
93 | - log('Generating template context for shared-db') |
94 | - conf = config() |
95 | - try: |
96 | - database = conf['database'] |
97 | - username = conf['database-user'] |
98 | - except KeyError as e: |
99 | + self.database = self.database or config('database') |
100 | + self.user = self.user or config('database-user') |
101 | + if None in [self.database, self.user]: |
102 | log('Could not generate shared_db context. ' |
103 | - 'Missing required charm config options: %s.' % e) |
104 | + 'Missing required charm config options. ' |
105 | + '(database name and user)') |
106 | raise OSContextError |
107 | ctxt = {} |
108 | + |
109 | + password_setting = 'password' |
110 | + if self.relation_prefix: |
111 | + password_setting = self.relation_prefix + '_password' |
112 | + |
113 | for rid in relation_ids('shared-db'): |
114 | for unit in related_units(rid): |
115 | + passwd = relation_get(password_setting, rid=rid, unit=unit) |
116 | ctxt = { |
117 | 'database_host': relation_get('db_host', rid=rid, |
118 | unit=unit), |
119 | - 'database': database, |
120 | - 'database_user': username, |
121 | - 'database_password': relation_get('password', rid=rid, |
122 | - unit=unit) |
123 | + 'database': self.database, |
124 | + 'database_user': self.user, |
125 | + 'database_password': passwd, |
126 | } |
127 | - if not context_complete(ctxt): |
128 | - return {} |
129 | - return ctxt |
130 | + if context_complete(ctxt): |
131 | + return ctxt |
132 | + return {} |
133 | |
134 | |
135 | class IdentityServiceContext(OSContextGenerator): |
136 | @@ -109,9 +143,9 @@ |
137 | 'service_protocol': 'http', |
138 | 'auth_protocol': 'http', |
139 | } |
140 | - if not context_complete(ctxt): |
141 | - return {} |
142 | - return ctxt |
143 | + if context_complete(ctxt): |
144 | + return ctxt |
145 | + return {} |
146 | |
147 | |
148 | class AMQPContext(OSContextGenerator): |
149 | @@ -132,20 +166,30 @@ |
150 | for rid in relation_ids('amqp'): |
151 | for unit in related_units(rid): |
152 | if relation_get('clustered', rid=rid, unit=unit): |
153 | - rabbitmq_host = relation_get('vip', rid=rid, unit=unit) |
154 | + ctxt['clustered'] = True |
155 | + ctxt['rabbitmq_host'] = relation_get('vip', rid=rid, |
156 | + unit=unit) |
157 | else: |
158 | - rabbitmq_host = relation_get('private-address', |
159 | - rid=rid, unit=unit) |
160 | - ctxt = { |
161 | - 'rabbitmq_host': rabbitmq_host, |
162 | + ctxt['rabbitmq_host'] = relation_get('private-address', |
163 | + rid=rid, unit=unit) |
164 | + ctxt.update({ |
165 | 'rabbitmq_user': username, |
166 | 'rabbitmq_password': relation_get('password', rid=rid, |
167 | unit=unit), |
168 | 'rabbitmq_virtual_host': vhost, |
169 | - } |
170 | + }) |
171 | + if context_complete(ctxt): |
172 | + # Sufficient information found = break out! |
173 | + break |
174 | + # Used for active/active rabbitmq >= grizzly |
175 | + ctxt['rabbitmq_hosts'] = [] |
176 | + for unit in related_units(rid): |
177 | + ctxt['rabbitmq_hosts'].append(relation_get('private-address', |
178 | + rid=rid, unit=unit)) |
179 | if not context_complete(ctxt): |
180 | return {} |
181 | - return ctxt |
182 | + else: |
183 | + return ctxt |
184 | |
185 | |
186 | class CephContext(OSContextGenerator): |
187 | @@ -153,21 +197,33 @@ |
188 | |
189 | def __call__(self): |
190 | '''This generates context for /etc/ceph/ceph.conf templates''' |
191 | - log('Generating tmeplate context for ceph') |
192 | + if not relation_ids('ceph'): |
193 | + return {} |
194 | + log('Generating template context for ceph') |
195 | mon_hosts = [] |
196 | auth = None |
197 | + key = None |
198 | for rid in relation_ids('ceph'): |
199 | for unit in related_units(rid): |
200 | mon_hosts.append(relation_get('private-address', rid=rid, |
201 | unit=unit)) |
202 | auth = relation_get('auth', rid=rid, unit=unit) |
203 | + key = relation_get('key', rid=rid, unit=unit) |
204 | |
205 | ctxt = { |
206 | 'mon_hosts': ' '.join(mon_hosts), |
207 | 'auth': auth, |
208 | + 'key': key, |
209 | } |
210 | + |
211 | + if not os.path.isdir('/etc/ceph'): |
212 | + os.mkdir('/etc/ceph') |
213 | + |
214 | if not context_complete(ctxt): |
215 | return {} |
216 | + |
217 | + ensure_packages(['ceph-common']) |
218 | + |
219 | return ctxt |
220 | |
221 | |
222 | @@ -207,7 +263,7 @@ |
223 | |
224 | |
225 | class ImageServiceContext(OSContextGenerator): |
226 | - interfaces = ['image-servce'] |
227 | + interfaces = ['image-service'] |
228 | |
229 | def __call__(self): |
230 | ''' |
231 | @@ -269,6 +325,7 @@ |
232 | if ca_cert: |
233 | with open(CA_CERT_PATH, 'w') as ca_out: |
234 | ca_out.write(b64decode(ca_cert)) |
235 | + check_call(['update-ca-certificates']) |
236 | |
237 | def __call__(self): |
238 | if isinstance(self.external_ports, basestring): |
239 | @@ -292,3 +349,174 @@ |
240 | portmap = (int(ext_port), int(int_port)) |
241 | ctxt['endpoints'].append(portmap) |
242 | return ctxt |
243 | + |
244 | + |
245 | +class NeutronContext(object): |
246 | + interfaces = [] |
247 | + |
248 | + @property |
249 | + def plugin(self): |
250 | + return None |
251 | + |
252 | + @property |
253 | + def network_manager(self): |
254 | + return None |
255 | + |
256 | + @property |
257 | + def packages(self): |
258 | + return neutron_plugin_attribute( |
259 | + self.plugin, 'packages', self.network_manager) |
260 | + |
261 | + @property |
262 | + def neutron_security_groups(self): |
263 | + return None |
264 | + |
265 | + def _ensure_packages(self): |
266 | + [ensure_packages(pkgs) for pkgs in self.packages] |
267 | + |
268 | + def _save_flag_file(self): |
269 | + if self.network_manager == 'quantum': |
270 | + _file = '/etc/nova/quantum_plugin.conf' |
271 | + else: |
272 | + _file = '/etc/nova/neutron_plugin.conf' |
273 | + with open(_file, 'wb') as out: |
274 | + out.write(self.plugin + '\n') |
275 | + |
276 | + def ovs_ctxt(self): |
277 | + driver = neutron_plugin_attribute(self.plugin, 'driver', |
278 | + self.network_manager) |
279 | + |
280 | + ovs_ctxt = { |
281 | + 'core_plugin': driver, |
282 | + 'neutron_plugin': 'ovs', |
283 | + 'neutron_security_groups': self.neutron_security_groups, |
284 | + 'local_ip': unit_private_ip(), |
285 | + } |
286 | + |
287 | + return ovs_ctxt |
288 | + |
289 | + def __call__(self): |
290 | + self._ensure_packages() |
291 | + |
292 | + if self.network_manager not in ['quantum', 'neutron']: |
293 | + return {} |
294 | + |
295 | + if not self.plugin: |
296 | + return {} |
297 | + |
298 | + ctxt = {'network_manager': self.network_manager} |
299 | + |
300 | + if self.plugin == 'ovs': |
301 | + ctxt.update(self.ovs_ctxt()) |
302 | + |
303 | + self._save_flag_file() |
304 | + return ctxt |
305 | + |
306 | + |
307 | +class OSConfigFlagContext(OSContextGenerator): |
308 | + ''' |
309 | + Responsible adding user-defined config-flags in charm config to a |
310 | + to a template context. |
311 | + ''' |
312 | + def __call__(self): |
313 | + config_flags = config('config-flags') |
314 | + if not config_flags or config_flags in ['None', '']: |
315 | + return {} |
316 | + config_flags = config_flags.split(',') |
317 | + flags = {} |
318 | + for flag in config_flags: |
319 | + if '=' not in flag: |
320 | + log('Improperly formatted config-flag, expected k=v ' |
321 | + 'got %s' % flag, level=WARNING) |
322 | + continue |
323 | + k, v = flag.split('=') |
324 | + flags[k.strip()] = v |
325 | + ctxt = {'user_config_flags': flags} |
326 | + return ctxt |
327 | + |
328 | + |
329 | +class SubordinateConfigContext(OSContextGenerator): |
330 | + """ |
331 | + Responsible for inspecting relations to subordinates that |
332 | + may be exporting required config via a json blob. |
333 | + |
334 | + The subordinate interface allows subordinates to export their |
335 | + configuration requirements to the principle for multiple config |
336 | + files and multiple serivces. Ie, a subordinate that has interfaces |
337 | + to both glance and nova may export to following yaml blob as json: |
338 | + |
339 | + glance: |
340 | + /etc/glance/glance-api.conf: |
341 | + sections: |
342 | + DEFAULT: |
343 | + - [key1, value1] |
344 | + /etc/glance/glance-registry.conf: |
345 | + MYSECTION: |
346 | + - [key2, value2] |
347 | + nova: |
348 | + /etc/nova/nova.conf: |
349 | + sections: |
350 | + DEFAULT: |
351 | + - [key3, value3] |
352 | + |
353 | + |
354 | + It is then up to the principle charms to subscribe this context to |
355 | + the service+config file it is interestd in. Configuration data will |
356 | + be available in the template context, in glance's case, as: |
357 | + ctxt = { |
358 | + ... other context ... |
359 | + 'subordinate_config': { |
360 | + 'DEFAULT': { |
361 | + 'key1': 'value1', |
362 | + }, |
363 | + 'MYSECTION': { |
364 | + 'key2': 'value2', |
365 | + }, |
366 | + } |
367 | + } |
368 | + |
369 | + """ |
370 | + def __init__(self, service, config_file, interface): |
371 | + """ |
372 | + :param service : Service name key to query in any subordinate |
373 | + data found |
374 | + :param config_file : Service's config file to query sections |
375 | + :param interface : Subordinate interface to inspect |
376 | + """ |
377 | + self.service = service |
378 | + self.config_file = config_file |
379 | + self.interface = interface |
380 | + |
381 | + def __call__(self): |
382 | + ctxt = {} |
383 | + for rid in relation_ids(self.interface): |
384 | + for unit in related_units(rid): |
385 | + sub_config = relation_get('subordinate_configuration', |
386 | + rid=rid, unit=unit) |
387 | + if sub_config and sub_config != '': |
388 | + try: |
389 | + sub_config = json.loads(sub_config) |
390 | + except: |
391 | + log('Could not parse JSON from subordinate_config ' |
392 | + 'setting from %s' % rid, level=ERROR) |
393 | + continue |
394 | + |
395 | + if self.service not in sub_config: |
396 | + log('Found subordinate_config on %s but it contained' |
397 | + 'nothing for %s service' % (rid, self.service)) |
398 | + continue |
399 | + |
400 | + sub_config = sub_config[self.service] |
401 | + if self.config_file not in sub_config: |
402 | + log('Found subordinate_config on %s but it contained' |
403 | + 'nothing for %s' % (rid, self.config_file)) |
404 | + continue |
405 | + |
406 | + sub_config = sub_config[self.config_file] |
407 | + for k, v in sub_config.iteritems(): |
408 | + ctxt[k] = v |
409 | + |
410 | + if not ctxt: |
411 | + ctxt['sections'] = {} |
412 | + |
413 | + return ctxt |
414 | |
415 | === added file 'charmhelpers/contrib/openstack/neutron.py' |
416 | --- charmhelpers/contrib/openstack/neutron.py 1970-01-01 00:00:00 +0000 |
417 | +++ charmhelpers/contrib/openstack/neutron.py 2013-10-15 01:15:14 +0000 |
418 | @@ -0,0 +1,117 @@ |
419 | +# Various utilies for dealing with Neutron and the renaming from Quantum. |
420 | + |
421 | +from subprocess import check_output |
422 | + |
423 | +from charmhelpers.core.hookenv import ( |
424 | + config, |
425 | + log, |
426 | + ERROR, |
427 | +) |
428 | + |
429 | +from charmhelpers.contrib.openstack.utils import os_release |
430 | + |
431 | + |
432 | +def headers_package(): |
433 | + """Ensures correct linux-headers for running kernel are installed, |
434 | + for building DKMS package""" |
435 | + kver = check_output(['uname', '-r']).strip() |
436 | + return 'linux-headers-%s' % kver |
437 | + |
438 | + |
439 | +# legacy |
440 | +def quantum_plugins(): |
441 | + from charmhelpers.contrib.openstack import context |
442 | + return { |
443 | + 'ovs': { |
444 | + 'config': '/etc/quantum/plugins/openvswitch/' |
445 | + 'ovs_quantum_plugin.ini', |
446 | + 'driver': 'quantum.plugins.openvswitch.ovs_quantum_plugin.' |
447 | + 'OVSQuantumPluginV2', |
448 | + 'contexts': [ |
449 | + context.SharedDBContext(user=config('neutron-database-user'), |
450 | + database=config('neutron-database'), |
451 | + relation_prefix='neutron')], |
452 | + 'services': ['quantum-plugin-openvswitch-agent'], |
453 | + 'packages': [[headers_package(), 'openvswitch-datapath-dkms'], |
454 | + ['quantum-plugin-openvswitch-agent']], |
455 | + }, |
456 | + 'nvp': { |
457 | + 'config': '/etc/quantum/plugins/nicira/nvp.ini', |
458 | + 'driver': 'quantum.plugins.nicira.nicira_nvp_plugin.' |
459 | + 'QuantumPlugin.NvpPluginV2', |
460 | + 'services': [], |
461 | + 'packages': [], |
462 | + } |
463 | + } |
464 | + |
465 | + |
466 | +def neutron_plugins(): |
467 | + from charmhelpers.contrib.openstack import context |
468 | + return { |
469 | + 'ovs': { |
470 | + 'config': '/etc/neutron/plugins/openvswitch/' |
471 | + 'ovs_neutron_plugin.ini', |
472 | + 'driver': 'neutron.plugins.openvswitch.ovs_neutron_plugin.' |
473 | + 'OVSNeutronPluginV2', |
474 | + 'contexts': [ |
475 | + context.SharedDBContext(user=config('neutron-database-user'), |
476 | + database=config('neutron-database'), |
477 | + relation_prefix='neutron')], |
478 | + 'services': ['neutron-plugin-openvswitch-agent'], |
479 | + 'packages': [[headers_package(), 'openvswitch-datapath-dkms'], |
480 | + ['quantum-plugin-openvswitch-agent']], |
481 | + }, |
482 | + 'nvp': { |
483 | + 'config': '/etc/neutron/plugins/nicira/nvp.ini', |
484 | + 'driver': 'neutron.plugins.nicira.nicira_nvp_plugin.' |
485 | + 'NeutronPlugin.NvpPluginV2', |
486 | + 'services': [], |
487 | + 'packages': [], |
488 | + } |
489 | + } |
490 | + |
491 | + |
492 | +def neutron_plugin_attribute(plugin, attr, net_manager=None): |
493 | + manager = net_manager or network_manager() |
494 | + if manager == 'quantum': |
495 | + plugins = quantum_plugins() |
496 | + elif manager == 'neutron': |
497 | + plugins = neutron_plugins() |
498 | + else: |
499 | + log('Error: Network manager does not support plugins.') |
500 | + raise Exception |
501 | + |
502 | + try: |
503 | + _plugin = plugins[plugin] |
504 | + except KeyError: |
505 | + log('Unrecognised plugin for %s: %s' % (manager, plugin), level=ERROR) |
506 | + raise Exception |
507 | + |
508 | + try: |
509 | + return _plugin[attr] |
510 | + except KeyError: |
511 | + return None |
512 | + |
513 | + |
514 | +def network_manager(): |
515 | + ''' |
516 | + Deals with the renaming of Quantum to Neutron in H and any situations |
517 | + that require compatability (eg, deploying H with network-manager=quantum, |
518 | + upgrading from G). |
519 | + ''' |
520 | + release = os_release('nova-common') |
521 | + manager = config('network-manager').lower() |
522 | + |
523 | + if manager not in ['quantum', 'neutron']: |
524 | + return manager |
525 | + |
526 | + if release in ['essex']: |
527 | + # E does not support neutron |
528 | + log('Neutron networking not supported in Essex.', level=ERROR) |
529 | + raise Exception |
530 | + elif release in ['folsom', 'grizzly']: |
531 | + # neutron is named quantum in F and G |
532 | + return 'quantum' |
533 | + else: |
534 | + # ensure accurate naming for all releases post-H |
535 | + return 'neutron' |
536 | |
537 | === added symlink 'charmhelpers/contrib/openstack/templates/openstack_https_frontend.conf' |
538 | === target is u'openstack_https_frontend' |
539 | === modified file 'charmhelpers/contrib/openstack/templating.py' |
540 | --- charmhelpers/contrib/openstack/templating.py 2013-08-07 23:14:14 +0000 |
541 | +++ charmhelpers/contrib/openstack/templating.py 2013-10-15 01:15:14 +0000 |
542 | @@ -11,10 +11,10 @@ |
543 | from charmhelpers.contrib.openstack.utils import OPENSTACK_CODENAMES |
544 | |
545 | try: |
546 | - from jinja2 import FileSystemLoader, ChoiceLoader, Environment |
547 | + from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions |
548 | except ImportError: |
549 | # python-jinja2 may not be installed yet, or we're running unittests. |
550 | - FileSystemLoader = ChoiceLoader = Environment = None |
551 | + FileSystemLoader = ChoiceLoader = Environment = exceptions = None |
552 | |
553 | |
554 | class OSConfigException(Exception): |
555 | @@ -220,9 +220,24 @@ |
556 | log('Config not registered: %s' % config_file, level=ERROR) |
557 | raise OSConfigException |
558 | ctxt = self.templates[config_file].context() |
559 | + |
560 | _tmpl = os.path.basename(config_file) |
561 | + try: |
562 | + template = self._get_template(_tmpl) |
563 | + except exceptions.TemplateNotFound: |
564 | + # if no template is found with basename, try looking for it |
565 | + # using a munged full path, eg: |
566 | + # /etc/apache2/apache2.conf -> etc_apache2_apache2.conf |
567 | + _tmpl = '_'.join(config_file.split('/')[1:]) |
568 | + try: |
569 | + template = self._get_template(_tmpl) |
570 | + except exceptions.TemplateNotFound as e: |
571 | + log('Could not load template from %s by %s or %s.' % |
572 | + (self.templates_dir, os.path.basename(config_file), _tmpl), |
573 | + level=ERROR) |
574 | + raise e |
575 | + |
576 | log('Rendering from template: %s' % _tmpl, level=INFO) |
577 | - template = self._get_template(_tmpl) |
578 | return template.render(ctxt) |
579 | |
580 | def write(self, config_file): |
581 | @@ -232,8 +247,12 @@ |
582 | if config_file not in self.templates: |
583 | log('Config not registered: %s' % config_file, level=ERROR) |
584 | raise OSConfigException |
585 | + |
586 | + _out = self.render(config_file) |
587 | + |
588 | with open(config_file, 'wb') as out: |
589 | - out.write(self.render(config_file)) |
590 | + out.write(_out) |
591 | + |
592 | log('Wrote template %s.' % config_file, level=INFO) |
593 | |
594 | def write_all(self): |
595 | |
596 | === modified file 'charmhelpers/contrib/openstack/utils.py' |
597 | --- charmhelpers/contrib/openstack/utils.py 2013-08-23 14:12:19 +0000 |
598 | +++ charmhelpers/contrib/openstack/utils.py 2013-10-15 01:15:14 +0000 |
599 | @@ -1,12 +1,12 @@ |
600 | #!/usr/bin/python |
601 | |
602 | # Common python helper functions used for OpenStack charms. |
603 | - |
604 | from collections import OrderedDict |
605 | |
606 | import apt_pkg as apt |
607 | import subprocess |
608 | import os |
609 | +import socket |
610 | import sys |
611 | |
612 | from charmhelpers.core.hookenv import ( |
613 | @@ -45,16 +45,17 @@ |
614 | ]) |
615 | |
616 | # The ugly duckling |
617 | -SWIFT_CODENAMES = { |
618 | - '1.4.3': 'diablo', |
619 | - '1.4.8': 'essex', |
620 | - '1.7.4': 'folsom', |
621 | - '1.7.6': 'grizzly', |
622 | - '1.7.7': 'grizzly', |
623 | - '1.8.0': 'grizzly', |
624 | - '1.9.0': 'havana', |
625 | - '1.9.1': 'havana', |
626 | -} |
627 | +SWIFT_CODENAMES = OrderedDict([ |
628 | + ('1.4.3', 'diablo'), |
629 | + ('1.4.8', 'essex'), |
630 | + ('1.7.4', 'folsom'), |
631 | + ('1.8.0', 'grizzly'), |
632 | + ('1.7.7', 'grizzly'), |
633 | + ('1.7.6', 'grizzly'), |
634 | + ('1.10.0', 'havana'), |
635 | + ('1.9.1', 'havana'), |
636 | + ('1.9.0', 'havana'), |
637 | +]) |
638 | |
639 | |
640 | def error_out(msg): |
641 | @@ -137,8 +138,11 @@ |
642 | |
643 | try: |
644 | if 'swift' in pkg.name: |
645 | - vers = vers[:5] |
646 | - return SWIFT_CODENAMES[vers] |
647 | + swift_vers = vers[:5] |
648 | + if swift_vers not in SWIFT_CODENAMES: |
649 | + # Deal with 1.10.0 upward |
650 | + swift_vers = vers[:6] |
651 | + return SWIFT_CODENAMES[swift_vers] |
652 | else: |
653 | vers = vers[:6] |
654 | return OPENSTACK_CODENAMES[vers] |
655 | @@ -166,6 +170,25 @@ |
656 | #error_out(e) |
657 | |
658 | |
659 | +os_rel = None |
660 | + |
661 | + |
662 | +def os_release(package, base='essex'): |
663 | + ''' |
664 | + Returns OpenStack release codename from a cached global. |
665 | + If the codename can not be determined from either an installed package or |
666 | + the installation source, the earliest release supported by the charm should |
667 | + be returned. |
668 | + ''' |
669 | + global os_rel |
670 | + if os_rel: |
671 | + return os_rel |
672 | + os_rel = (get_os_codename_package(package, fatal=False) or |
673 | + get_os_codename_install_source(config('openstack-origin')) or |
674 | + base) |
675 | + return os_rel |
676 | + |
677 | + |
678 | def import_key(keyid): |
679 | cmd = "apt-key adv --keyserver keyserver.ubuntu.com " \ |
680 | "--recv-keys %s" % keyid |
681 | @@ -274,3 +297,69 @@ |
682 | available_vers = get_os_version_install_source(src) |
683 | apt.init() |
684 | return apt.version_compare(available_vers, cur_vers) == 1 |
685 | + |
686 | + |
687 | +def is_ip(address): |
688 | + """ |
689 | + Returns True if address is a valid IP address. |
690 | + """ |
691 | + try: |
692 | + # Test to see if already an IPv4 address |
693 | + socket.inet_aton(address) |
694 | + return True |
695 | + except socket.error: |
696 | + return False |
697 | + |
698 | + |
699 | +def ns_query(address): |
700 | + try: |
701 | + import dns.resolver |
702 | + except ImportError: |
703 | + apt_install('python-dnspython') |
704 | + import dns.resolver |
705 | + |
706 | + if isinstance(address, dns.name.Name): |
707 | + rtype = 'PTR' |
708 | + elif isinstance(address, basestring): |
709 | + rtype = 'A' |
710 | + |
711 | + answers = dns.resolver.query(address, rtype) |
712 | + if answers: |
713 | + return str(answers[0]) |
714 | + return None |
715 | + |
716 | + |
717 | +def get_host_ip(hostname): |
718 | + """ |
719 | + Resolves the IP for a given hostname, or returns |
720 | + the input if it is already an IP. |
721 | + """ |
722 | + if is_ip(hostname): |
723 | + return hostname |
724 | + |
725 | + return ns_query(hostname) |
726 | + |
727 | + |
728 | +def get_hostname(address): |
729 | + """ |
730 | + Resolves hostname for given IP, or returns the input |
731 | + if it is already a hostname. |
732 | + """ |
733 | + if not is_ip(address): |
734 | + return address |
735 | + |
736 | + try: |
737 | + import dns.reversename |
738 | + except ImportError: |
739 | + apt_install('python-dnspython') |
740 | + import dns.reversename |
741 | + |
742 | + rev = dns.reversename.from_address(address) |
743 | + result = ns_query(rev) |
744 | + if not result: |
745 | + return None |
746 | + |
747 | + # strip trailing . |
748 | + if result.endswith('.'): |
749 | + return result[:-1] |
750 | + return result |
751 | |
752 | === modified file 'charmhelpers/contrib/storage/linux/ceph.py' |
753 | --- charmhelpers/contrib/storage/linux/ceph.py 2013-09-24 14:46:53 +0000 |
754 | +++ charmhelpers/contrib/storage/linux/ceph.py 2013-10-15 01:15:14 +0000 |
755 | @@ -335,3 +335,25 @@ |
756 | log('ceph: Starting service {} after migrating data.' |
757 | .format(svc)) |
758 | service_start(svc) |
759 | + |
760 | + |
761 | +def ensure_ceph_keyring(service, user=None, group=None): |
762 | + ''' |
763 | + Ensures a ceph keyring is created for a named service |
764 | + and optionally ensures user and group ownership. |
765 | + |
766 | + Returns False if no ceph key is available in relation state. |
767 | + ''' |
768 | + key = None |
769 | + for rid in relation_ids('ceph'): |
770 | + for unit in related_units(rid): |
771 | + key = relation_get('key', rid=rid, unit=unit) |
772 | + if key: |
773 | + break |
774 | + if not key: |
775 | + return False |
776 | + create_keyring(service=service, key=key) |
777 | + keyring = _keyring_path(service) |
778 | + if user and group: |
779 | + check_call(['chown', '%s.%s' % (user, group), keyring]) |
780 | + return True |
781 | |
782 | === modified file 'tests/contrib/openstack/test_openstack_utils.py' |
783 | --- tests/contrib/openstack/test_openstack_utils.py 2013-07-18 11:13:34 +0000 |
784 | +++ tests/contrib/openstack/test_openstack_utils.py 2013-10-15 01:15:14 +0000 |
785 | @@ -43,6 +43,11 @@ |
786 | 'os_release': 'havana', |
787 | 'os_version': '1.9.0' |
788 | }, |
789 | + 'swift-proxy': { |
790 | + 'pkg_vers': '1.10.0~rc1-0ubuntu1', |
791 | + 'os_release': 'havana', |
792 | + 'os_version': '1.10.0' |
793 | + }, |
794 | # a package thats available in the cache but is not installed |
795 | 'cinder-common': { |
796 | 'os_release': 'havana', |
797 | @@ -71,6 +76,36 @@ |
798 | ] |
799 | |
800 | |
801 | +# Mock python-dnspython resolver used by get_host_ip() |
802 | +class FakeAnswer(object): |
803 | + def __init__(self, ip): |
804 | + self.ip = ip |
805 | + |
806 | + def __str__(self): |
807 | + return self.ip |
808 | + |
809 | + |
810 | +class FakeResolver(object): |
811 | + def __init__(self, ip): |
812 | + self.ip = ip |
813 | + |
814 | + def query(self, hostname, query_type): |
815 | + return [FakeAnswer(self.ip)] |
816 | + |
817 | + |
818 | +class FakeReverse(object): |
819 | + def from_address(self, address): |
820 | + return '156.94.189.91.in-addr.arpa' |
821 | + |
822 | + |
823 | +class FakeDNS(object): |
824 | + def __init__(self, ip): |
825 | + self.resolver = FakeResolver(ip) |
826 | + self.reversename = FakeReverse() |
827 | + self.name = MagicMock() |
828 | + self.name.Name = basestring |
829 | + |
830 | + |
831 | class OpenStackHelpersTestCase(TestCase): |
832 | def _apt_cache(self): |
833 | # mocks out the apt cache |
834 | @@ -266,6 +301,16 @@ |
835 | openstack.get_os_version_package('foo', fatal=False) |
836 | ) |
837 | |
838 | + @patch.object(openstack, 'get_os_codename_package') |
839 | + def test_os_release_uncached(self, get_cn): |
840 | + openstack.os_rel = None |
841 | + get_cn.return_value = 'folsom' |
842 | + self.assertEquals('folsom', openstack.os_release('nova-common')) |
843 | + |
844 | + def test_os_release_cached(self): |
845 | + openstack.os_rel = 'foo' |
846 | + self.assertEquals('foo', openstack.os_release('nova-common')) |
847 | + |
848 | @patch.object(openstack, 'juju_log') |
849 | @patch('sys.exit') |
850 | def test_error_out(self, mocked_exit, juju_log): |
851 | @@ -371,10 +416,11 @@ |
852 | '--recv-keys', 'foo'] |
853 | mocked_error.assert_called_with('Error importing repo key foo') |
854 | |
855 | + @patch('os.mkdir') |
856 | @patch('os.path.exists') |
857 | @patch('charmhelpers.contrib.openstack.utils.charm_dir') |
858 | @patch('__builtin__.open') |
859 | - def test_save_scriptrc(self, _open, _charm_dir, _exists): |
860 | + def test_save_scriptrc(self, _open, _charm_dir, _exists, _mkdir): |
861 | '''Test generation of scriptrc from environment''' |
862 | scriptrc = ['#!/bin/bash\n', |
863 | 'export setting1=foo\n', |
864 | @@ -382,11 +428,12 @@ |
865 | _file = MagicMock(spec=file) |
866 | _open.return_value = _file |
867 | _charm_dir.return_value = '/var/lib/juju/units/testing-foo-0/charm' |
868 | - _exists.return_value = True |
869 | + _exists.return_value = False |
870 | os.environ['JUJU_UNIT_NAME'] = 'testing-foo/0' |
871 | openstack.save_script_rc(setting1='foo', setting2='bar') |
872 | expected_f = '/var/lib/juju/units/testing-foo-0/charm/scripts/scriptrc' |
873 | _open.assert_called_with(expected_f, 'wb') |
874 | + _mkdir.assert_called_with(os.path.dirname(expected_f)) |
875 | for line in scriptrc: |
876 | _file.__enter__().write.assert_has_calls(call(line)) |
877 | |
878 | @@ -416,6 +463,37 @@ |
879 | vers_pkg.return_value = '2013.1~b1' |
880 | self.assertFalse(openstack.openstack_upgrade_available('nova-common')) |
881 | |
882 | + def test_is_ip(self): |
883 | + self.assertTrue(openstack.is_ip('10.0.0.1')) |
884 | + self.assertFalse(openstack.is_ip('www.ubuntu.com')) |
885 | + |
886 | + @patch.object(openstack, 'apt_install') |
887 | + def test_get_host_ip_with_hostname(self, apt_install): |
888 | + fake_dns = FakeDNS('10.0.0.1') |
889 | + with patch('__builtin__.__import__', side_effect=[fake_dns]): |
890 | + ip = openstack.get_host_ip('www.ubuntu.com') |
891 | + self.assertEquals(ip, '10.0.0.1') |
892 | + |
893 | + @patch.object(openstack, 'apt_install') |
894 | + def test_get_host_ip_with_ip(self, apt_install): |
895 | + fake_dns = FakeDNS('5.5.5.5') |
896 | + with patch('__builtin__.__import__', side_effect=[fake_dns]): |
897 | + ip = openstack.get_host_ip('4.2.2.1') |
898 | + self.assertEquals(ip, '4.2.2.1') |
899 | + |
900 | + @patch.object(openstack, 'apt_install') |
901 | + def test_get_hostname_with_ip(self, apt_install): |
902 | + fake_dns = FakeDNS('www.ubuntu.com') |
903 | + with patch('__builtin__.__import__', side_effect=[fake_dns, fake_dns]): |
904 | + hn = openstack.get_hostname('4.2.2.1') |
905 | + self.assertEquals(hn, 'www.ubuntu.com') |
906 | + |
907 | + @patch.object(openstack, 'apt_install') |
908 | + def test_get_hostname_with_hostname(self, apt_install): |
909 | + fake_dns = FakeDNS('5.5.5.5') |
910 | + with patch('__builtin__.__import__', side_effect=[fake_dns]): |
911 | + hn = openstack.get_hostname('www.ubuntu.com') |
912 | + self.assertEquals(hn, 'www.ubuntu.com') |
913 | |
914 | if __name__ == '__main__': |
915 | unittest.main() |
916 | |
917 | === modified file 'tests/contrib/openstack/test_os_contexts.py' |
918 | --- tests/contrib/openstack/test_os_contexts.py 2013-07-19 23:29:59 +0000 |
919 | +++ tests/contrib/openstack/test_os_contexts.py 2013-10-15 01:15:14 +0000 |
920 | @@ -1,3 +1,5 @@ |
921 | +import yaml |
922 | +import json |
923 | import unittest |
924 | |
925 | from mock import patch, MagicMock, call |
926 | @@ -109,6 +111,18 @@ |
927 | 'vip': '10.0.0.1', |
928 | } |
929 | |
930 | +AMQP_AA_RELATION = { |
931 | + 'amqp:0': { |
932 | + 'rabbitmq/0': { |
933 | + 'private-address': 'rabbithost1', |
934 | + 'password': 'foobar', |
935 | + }, |
936 | + 'rabbitmq/1': { |
937 | + 'private-address': 'rabbithost2', |
938 | + } |
939 | + } |
940 | +} |
941 | + |
942 | AMQP_CONFIG = { |
943 | 'rabbit-user': 'adam', |
944 | 'rabbit-vhost': 'foo', |
945 | @@ -119,14 +133,51 @@ |
946 | 'ceph/0': { |
947 | 'private-address': 'ceph_node1', |
948 | 'auth': 'foo', |
949 | + 'key': 'bar', |
950 | }, |
951 | 'ceph/1': { |
952 | 'private-address': 'ceph_node2', |
953 | 'auth': 'foo', |
954 | - }, |
955 | - } |
956 | -} |
957 | - |
958 | + 'key': 'bar', |
959 | + }, |
960 | + } |
961 | +} |
962 | + |
963 | +SUB_CONFIG = """ |
964 | +nova: |
965 | + /etc/nova/nova.conf: |
966 | + sections: |
967 | + DEFAULT: |
968 | + - [nova-key1, value1] |
969 | + - [nova-key2, value2] |
970 | +glance: |
971 | + /etc/glance/glance.conf: |
972 | + sections: |
973 | + DEFAULT: |
974 | + - [glance-key1, value1] |
975 | + - [glance-key2, value2] |
976 | +""" |
977 | + |
978 | +SUB_CONFIG_RELATION = { |
979 | + 'nova-subordinate:0': { |
980 | + 'nova-subordinate/0': { |
981 | + 'private-address': 'nova_node1', |
982 | + 'subordinate_configuration': json.dumps(yaml.load(SUB_CONFIG)), |
983 | + }, |
984 | + }, |
985 | + 'glance-subordinate:0': { |
986 | + 'glance-subordinate/0': { |
987 | + 'private-address': 'glance_node1', |
988 | + 'subordinate_configuration': json.dumps(yaml.load(SUB_CONFIG)), |
989 | + }, |
990 | + }, |
991 | + 'foo-subordinate:0': { |
992 | + 'foo-subordinate/0': { |
993 | + 'private-address': 'foo_node1', |
994 | + 'subordinate_configuration': 'ea8e09324jkadsfh', |
995 | + }, |
996 | + } |
997 | +} |
998 | |
999 | # Imported in contexts.py and needs patching in setUp() |
1000 | TO_PATCH = [ |
1001 | @@ -148,6 +199,16 @@ |
1002 | ] |
1003 | |
1004 | |
1005 | +class fake_config(object): |
1006 | + def __init__(self, data): |
1007 | + self.data = data |
1008 | + |
1009 | + def __call__(self, attr): |
1010 | + if attr in self.data: |
1011 | + return self.data[attr] |
1012 | + return None |
1013 | + |
1014 | + |
1015 | class ContextTests(unittest.TestCase): |
1016 | def setUp(self): |
1017 | for m in TO_PATCH: |
1018 | @@ -170,7 +231,7 @@ |
1019 | '''Test shared-db context with all required data''' |
1020 | relation = FakeRelation(relation_data=SHARED_DB_RELATION) |
1021 | self.relation_get.side_effect = relation.get |
1022 | - self.config.return_value = SHARED_DB_CONFIG |
1023 | + self.config.side_effect = fake_config(SHARED_DB_CONFIG) |
1024 | shared_db = context.SharedDBContext() |
1025 | result = shared_db() |
1026 | expected = { |
1027 | @@ -196,12 +257,23 @@ |
1028 | '''Test shared-db context missing relation data''' |
1029 | incomplete_config = copy(SHARED_DB_CONFIG) |
1030 | del incomplete_config['database-user'] |
1031 | + self.config.side_effect = fake_config(incomplete_config) |
1032 | relation = FakeRelation(relation_data=SHARED_DB_RELATION) |
1033 | self.relation_get.side_effect = relation.get |
1034 | self.config.return_value = incomplete_config |
1035 | shared_db = context.SharedDBContext() |
1036 | self.assertRaises(context.OSContextError, shared_db) |
1037 | |
1038 | + def test_shared_db_context_with_params(self): |
1039 | + '''Test shared-db context with object parameters''' |
1040 | + shared_db = context.SharedDBContext( |
1041 | + database='quantum', user='quantum', relation_prefix='quantum') |
1042 | + result = shared_db() |
1043 | + self.assertIn(call('quantum_password', rid='foo:0', unit='foo/0'), |
1044 | + self.relation_get.call_args_list) |
1045 | + self.assertEquals(result['database'], 'quantum') |
1046 | + self.assertEquals(result['database_user'], 'quantum') |
1047 | + |
1048 | def test_identity_service_context_with_data(self): |
1049 | '''Test shared-db context with all required data''' |
1050 | relation = FakeRelation(relation_data=IDENTITY_SERVICE_RELATION) |
1051 | @@ -238,12 +310,12 @@ |
1052 | self.config.return_value = AMQP_CONFIG |
1053 | amqp = context.AMQPContext() |
1054 | result = amqp() |
1055 | - |
1056 | expected = { |
1057 | 'rabbitmq_host': 'rabbithost', |
1058 | 'rabbitmq_password': 'foobar', |
1059 | 'rabbitmq_user': 'adam', |
1060 | - 'rabbitmq_virtual_host': 'foo' |
1061 | + 'rabbitmq_virtual_host': 'foo', |
1062 | + 'rabbitmq_hosts': ['rabbithost'], |
1063 | } |
1064 | self.assertEquals(result, expected) |
1065 | |
1066 | @@ -256,12 +328,32 @@ |
1067 | self.config.return_value = AMQP_CONFIG |
1068 | amqp = context.AMQPContext() |
1069 | result = amqp() |
1070 | - |
1071 | expected = { |
1072 | + 'clustered': True, |
1073 | 'rabbitmq_host': relation_data['vip'], |
1074 | 'rabbitmq_password': 'foobar', |
1075 | 'rabbitmq_user': 'adam', |
1076 | - 'rabbitmq_virtual_host': 'foo' |
1077 | + 'rabbitmq_virtual_host': 'foo', |
1078 | + 'rabbitmq_hosts': ['rabbithost'], |
1079 | + } |
1080 | + self.assertEquals(result, expected) |
1081 | + |
1082 | + def test_amqp_context_with_data_active_active(self): |
1083 | + '''Test amqp context with required data with active/active rabbit''' |
1084 | + relation_data = copy(AMQP_AA_RELATION) |
1085 | + relation = FakeRelation(relation_data=relation_data) |
1086 | + self.relation_get.side_effect = relation.get |
1087 | + self.relation_ids.side_effect = relation.relation_ids |
1088 | + self.related_units.side_effect = relation.relation_units |
1089 | + self.config.return_value = AMQP_CONFIG |
1090 | + amqp = context.AMQPContext() |
1091 | + result = amqp() |
1092 | + expected = { |
1093 | + 'rabbitmq_host': 'rabbithost1', |
1094 | + 'rabbitmq_password': 'foobar', |
1095 | + 'rabbitmq_user': 'adam', |
1096 | + 'rabbitmq_virtual_host': 'foo', |
1097 | + 'rabbitmq_hosts': ['rabbithost2', 'rabbithost1'], |
1098 | } |
1099 | self.assertEquals(result, expected) |
1100 | |
1101 | @@ -286,8 +378,12 @@ |
1102 | amqp = context.AMQPContext() |
1103 | self.assertRaises(context.OSContextError, amqp) |
1104 | |
1105 | - def test_ceph_context_with_data(self): |
1106 | + @patch('os.path.isdir') |
1107 | + @patch('os.mkdir') |
1108 | + @patch.object(context, 'ensure_packages') |
1109 | + def test_ceph_context_with_data(self, ensure_packages, mkdir, isdir): |
1110 | '''Test ceph context with all relation data''' |
1111 | + isdir.return_value = False |
1112 | relation = FakeRelation(relation_data=CEPH_RELATION) |
1113 | self.relation_get.side_effect = relation.get |
1114 | self.relation_ids.side_effect = relation.relation_ids |
1115 | @@ -296,11 +392,16 @@ |
1116 | result = ceph() |
1117 | expected = { |
1118 | 'mon_hosts': 'ceph_node2 ceph_node1', |
1119 | - 'auth': 'foo' |
1120 | + 'auth': 'foo', |
1121 | + 'key': 'bar', |
1122 | } |
1123 | self.assertEquals(result, expected) |
1124 | + ensure_packages.assert_called_with(['ceph-common']) |
1125 | + mkdir.assert_called_with('/etc/ceph') |
1126 | |
1127 | - def test_ceph_context_with_missing_data(self): |
1128 | + @patch('os.mkdir') |
1129 | + @patch.object(context, 'ensure_packages') |
1130 | + def test_ceph_context_with_missing_data(self, ensure_packages, mkdir): |
1131 | '''Test ceph context with missing relation data''' |
1132 | relation = copy(CEPH_RELATION) |
1133 | for k, v in relation.iteritems(): |
1134 | @@ -313,6 +414,7 @@ |
1135 | ceph = context.CephContext() |
1136 | result = ceph() |
1137 | self.assertEquals(result, {}) |
1138 | + self.assertFalse(ensure_packages.called) |
1139 | |
1140 | @patch('charmhelpers.contrib.openstack.context.unit_get') |
1141 | @patch('charmhelpers.contrib.openstack.context.local_unit') |
1142 | @@ -471,3 +573,146 @@ |
1143 | self.relation_get.return_value = 'http://glancehost:9292' |
1144 | self.assertEquals({'glance_api_servers': 'http://glancehost:9292'}, |
1145 | image_service()) |
1146 | + |
1147 | + @patch.object(context, 'neutron_plugin_attribute') |
1148 | + def test_neutron_context_base_properties(self, attr): |
1149 | + '''Test neutron context base properties''' |
1150 | + neutron = context.NeutronContext() |
1151 | + attr.return_value = 'quantum-plugin-package' |
1152 | + self.assertEquals(None, neutron.plugin) |
1153 | + self.assertEquals(None, neutron.network_manager) |
1154 | + self.assertEquals(None, neutron.neutron_security_groups) |
1155 | + self.assertEquals('quantum-plugin-package', neutron.packages) |
1156 | + |
1157 | + @patch.object(context, 'neutron_plugin_attribute') |
1158 | + @patch.object(context, 'apt_install') |
1159 | + @patch.object(context, 'filter_installed_packages') |
1160 | + def test_neutron_ensure_package(self, _filter, _install, _packages): |
1161 | + '''Test neutron context installed required packages''' |
1162 | + _filter.return_value = ['quantum-plugin-package'] |
1163 | + _packages.return_value = [['quantum-plugin-package']] |
1164 | + neutron = context.NeutronContext() |
1165 | + neutron._ensure_packages() |
1166 | + _install.assert_called_with(['quantum-plugin-package'], fatal=True) |
1167 | + |
1168 | + @patch.object(context.NeutronContext, 'network_manager') |
1169 | + @patch.object(context.NeutronContext, 'plugin') |
1170 | + def test_neutron_save_flag_file(self, plugin, nm): |
1171 | + neutron = context.NeutronContext() |
1172 | + plugin.__get__ = MagicMock(return_value='ovs') |
1173 | + nm.__get__ = MagicMock(return_value='quantum') |
1174 | + with patch_open() as (_o, _f): |
1175 | + neutron._save_flag_file() |
1176 | + _o.assert_called_with('/etc/nova/quantum_plugin.conf', 'wb') |
1177 | + _f.write.assert_called_with('ovs\n') |
1178 | + |
1179 | + nm.__get__ = MagicMock(return_value='neutron') |
1180 | + with patch_open() as (_o, _f): |
1181 | + neutron._save_flag_file() |
1182 | + _o.assert_called_with('/etc/nova/neutron_plugin.conf', 'wb') |
1183 | + _f.write.assert_called_with('ovs\n') |
1184 | + |
1185 | + @patch.object(context.NeutronContext, 'neutron_security_groups') |
1186 | + @patch.object(context, 'unit_private_ip') |
1187 | + @patch.object(context, 'neutron_plugin_attribute') |
1188 | + def test_neutron_ovs_plugin_context(self, attr, ip, sec_groups): |
1189 | + ip.return_value = '10.0.0.1' |
1190 | + sec_groups.__get__ = MagicMock(return_value=True) |
1191 | + attr.return_value = 'some.quantum.driver.class' |
1192 | + neutron = context.NeutronContext() |
1193 | + self.assertEquals({ |
1194 | + 'core_plugin': 'some.quantum.driver.class', |
1195 | + 'neutron_plugin': 'ovs', |
1196 | + 'neutron_security_groups': True, |
1197 | + 'local_ip': '10.0.0.1'}, neutron.ovs_ctxt()) |
1198 | + |
1199 | + @patch.object(context.NeutronContext, '_save_flag_file') |
1200 | + @patch.object(context.NeutronContext, 'ovs_ctxt') |
1201 | + @patch.object(context.NeutronContext, 'plugin') |
1202 | + @patch.object(context.NeutronContext, '_ensure_packages') |
1203 | + @patch.object(context.NeutronContext, 'network_manager') |
1204 | + def test_neutron_main_context_generation(self, nm, pkgs, plugin, ovs, ff): |
1205 | + neutron = context.NeutronContext() |
1206 | + nm.__get__ = MagicMock(return_value='flatdhcpmanager') |
1207 | + self.assertEquals({}, neutron()) |
1208 | + |
1209 | + nm.__get__ = MagicMock(return_value='neutron') |
1210 | + plugin.__get__ = MagicMock(return_value=None) |
1211 | + self.assertEquals({}, neutron()) |
1212 | + |
1213 | + nm.__get__ = MagicMock(return_value='neutron') |
1214 | + ovs.return_value = {'ovs': 'ovs_context'} |
1215 | + plugin.__get__ = MagicMock(return_value='ovs') |
1216 | + self.assertEquals( |
1217 | + {'network_manager': 'neutron', 'ovs': 'ovs_context'}, |
1218 | + neutron() |
1219 | + ) |
1220 | + |
1221 | + @patch.object(context, 'config') |
1222 | + def test_os_configflag_context(self, config): |
1223 | + flags = context.OSConfigFlagContext() |
1224 | + |
1225 | + config.return_value = 'floating_ip=True,use_virtio=False,max=5' |
1226 | + self.assertEquals({ |
1227 | + 'user_config_flags': { |
1228 | + 'floating_ip': 'True', |
1229 | + 'use_virtio': 'False', |
1230 | + 'max': '5', |
1231 | + } |
1232 | + }, flags()) |
1233 | + |
1234 | + for empty in [None, '']: |
1235 | + config.return_value = empty |
1236 | + self.assertEquals({}, flags()) |
1237 | + |
1238 | + config.return_value = 'good_flag=woot,badflag,great_flag=w00t' |
1239 | + self.assertEquals({ |
1240 | + 'user_config_flags': { |
1241 | + 'good_flag': 'woot', |
1242 | + 'great_flag': 'w00t', |
1243 | + } |
1244 | + }, flags()) |
1245 | + |
1246 | + def test_os_subordinate_config_context(self): |
1247 | + relation = FakeRelation(relation_data=SUB_CONFIG_RELATION) |
1248 | + self.relation_get.side_effect = relation.get |
1249 | + self.relation_ids.side_effect = relation.relation_ids |
1250 | + self.related_units.side_effect = relation.relation_units |
1251 | + nova_sub_ctxt = context.SubordinateConfigContext( |
1252 | + service='nova', |
1253 | + config_file='/etc/nova/nova.conf', |
1254 | + interface='nova-subordinate', |
1255 | + ) |
1256 | + glance_sub_ctxt = context.SubordinateConfigContext( |
1257 | + service='glance', |
1258 | + config_file='/etc/glance/glance.conf', |
1259 | + interface='glance-subordinate', |
1260 | + ) |
1261 | + foo_sub_ctxt = context.SubordinateConfigContext( |
1262 | + service='foo', |
1263 | + config_file='/etc/foo/foo.conf', |
1264 | + interface='foo-subordinate', |
1265 | + ) |
1266 | + self.assertEquals( |
1267 | + nova_sub_ctxt(), |
1268 | + {'sections': { |
1269 | + 'DEFAULT': [ |
1270 | + ['nova-key1', 'value1'], |
1271 | + ['nova-key2', 'value2']] |
1272 | + }} |
1273 | + ) |
1274 | + self.assertEquals( |
1275 | + glance_sub_ctxt(), |
1276 | + {'sections': { |
1277 | + 'DEFAULT': [ |
1278 | + ['glance-key1', 'value1'], |
1279 | + ['glance-key2', 'value2']] |
1280 | + }} |
1281 | + ) |
1282 | + |
1283 | + # subrodinate supplies nothing for given config |
1284 | + glance_sub_ctxt.config_file = '/etc/glance/glance-api-paste.ini' |
1285 | + self.assertEquals(glance_sub_ctxt(), {'sections': {}}) |
1286 | + |
1287 | + # subordinate supplies bad input |
1288 | + self.assertEquals(foo_sub_ctxt(), {'sections': {}}) |
1289 | |
1290 | === modified file 'tests/contrib/openstack/test_os_templating.py' |
1291 | --- tests/contrib/openstack/test_os_templating.py 2013-07-11 19:51:05 +0000 |
1292 | +++ tests/contrib/openstack/test_os_templating.py 2013-10-15 01:15:14 +0000 |
1293 | @@ -6,6 +6,8 @@ |
1294 | |
1295 | import charmhelpers.contrib.openstack.templating as templating |
1296 | |
1297 | +from jinja2.exceptions import TemplateNotFound |
1298 | + |
1299 | |
1300 | class FakeContextGenerator(object): |
1301 | interfaces = None |
1302 | @@ -114,6 +116,50 @@ |
1303 | fake_tmpl.render.assert_called_with({}) |
1304 | self.assertNotIn('fooservice', self.renderer.complete_contexts()) |
1305 | |
1306 | + def test_render_template_registered_but_not_found(self): |
1307 | + '''It loads a template by basename of config file first''' |
1308 | + path = os.path.dirname(__file__) |
1309 | + renderer = templating.OSConfigRenderer(templates_dir=path, |
1310 | + openstack_release='folsom') |
1311 | + e = TemplateNotFound('') |
1312 | + renderer._get_template = MagicMock() |
1313 | + renderer._get_template.side_effect = e |
1314 | + renderer.register('/etc/nova/nova.conf', contexts=[]) |
1315 | + self.assertRaises( |
1316 | + TemplateNotFound, renderer.render, '/etc/nova/nova.conf') |
1317 | + |
1318 | + def test_render_template_by_basename_first(self): |
1319 | + '''It loads a template by basename of config file first''' |
1320 | + path = os.path.dirname(__file__) |
1321 | + renderer = templating.OSConfigRenderer(templates_dir=path, |
1322 | + openstack_release='folsom') |
1323 | + renderer._get_template = MagicMock() |
1324 | + renderer.register('/etc/nova/nova.conf', contexts=[]) |
1325 | + renderer.render('/etc/nova/nova.conf') |
1326 | + self.assertEquals(1, len(renderer._get_template.call_args_list)) |
1327 | + self.assertEquals( |
1328 | + [call('nova.conf')], renderer._get_template.call_args_list) |
1329 | + |
1330 | + def test_render_template_by_munged_full_path_last(self): |
1331 | + '''It loads a template by full path of config file second''' |
1332 | + path = os.path.dirname(__file__) |
1333 | + renderer = templating.OSConfigRenderer(templates_dir=path, |
1334 | + openstack_release='folsom') |
1335 | + tmp = MagicMock() |
1336 | + tmp.render = MagicMock() |
1337 | + e = TemplateNotFound('') |
1338 | + renderer._get_template = MagicMock() |
1339 | + renderer._get_template.side_effect = [e, tmp] |
1340 | + renderer.register('/etc/nova/nova.conf', contexts=[]) |
1341 | + renderer.render('/etc/nova/nova.conf') |
1342 | + self.assertEquals(2, len(renderer._get_template.call_args_list)) |
1343 | + self.assertEquals( |
1344 | + [call('nova.conf'), call('etc_nova_nova.conf')], |
1345 | + renderer._get_template.call_args_list) |
1346 | + |
1347 | + def test_render_template_by_basename(self): |
1348 | + '''It renders template if it finds it by config file basename''' |
1349 | + |
1350 | @patch('__builtin__.open') |
1351 | @patch.object(templating, 'get_loader') |
1352 | def test_write_out_config(self, loader, _open): |
1353 | |
1354 | === modified file 'tests/contrib/storage/test_linux_ceph.py' |
1355 | --- tests/contrib/storage/test_linux_ceph.py 2013-09-24 14:46:53 +0000 |
1356 | +++ tests/contrib/storage/test_linux_ceph.py 2013-10-15 01:15:14 +0000 |
1357 | @@ -460,3 +460,43 @@ |
1358 | 'ceph: Formatting block device %s as ' |
1359 | 'filesystem %s.' % (device, fstype), level='INFO' |
1360 | ) |
1361 | + |
1362 | + @patch.object(ceph_utils, 'relation_ids') |
1363 | + @patch.object(ceph_utils, 'related_units') |
1364 | + @patch.object(ceph_utils, 'relation_get') |
1365 | + def test_ensure_ceph_keyring_no_relation_no_data(self, rget, runits, rids): |
1366 | + rids.return_value = [] |
1367 | + self.assertEquals(False, ceph_utils.ensure_ceph_keyring(service='foo')) |
1368 | + rids.return_value = ['ceph:0'] |
1369 | + runits.return_value = ['ceph/0'] |
1370 | + rget.return_value = '' |
1371 | + self.assertEquals(False, ceph_utils.ensure_ceph_keyring(service='foo')) |
1372 | + |
1373 | + @patch.object(ceph_utils, '_keyring_path') |
1374 | + @patch.object(ceph_utils, 'create_keyring') |
1375 | + @patch.object(ceph_utils, 'relation_ids') |
1376 | + @patch.object(ceph_utils, 'related_units') |
1377 | + @patch.object(ceph_utils, 'relation_get') |
1378 | + def test_ensure_ceph_keyring_with_data(self, rget, runits, |
1379 | + rids, create, _path): |
1380 | + rids.return_value = ['ceph:0'] |
1381 | + runits.return_value = ['ceph/0'] |
1382 | + rget.return_value = 'fookey' |
1383 | + self.assertEquals(True, |
1384 | + ceph_utils.ensure_ceph_keyring(service='foo')) |
1385 | + create.assert_called_with(service='foo', key='fookey') |
1386 | + _path.assert_called_with('foo') |
1387 | + self.assertFalse(self.check_call.called) |
1388 | + |
1389 | + _path.return_value = '/etc/ceph/client.foo.keyring' |
1390 | + self.assertEquals( |
1391 | + True, |
1392 | + ceph_utils.ensure_ceph_keyring( |
1393 | + service='foo', user='adam', group='users')) |
1394 | + create.assert_called_with(service='foo', key='fookey') |
1395 | + _path.assert_called_with('foo') |
1396 | + self.check_call.assert_called_with([ |
1397 | + 'chown', |
1398 | + 'adam.users', |
1399 | + '/etc/ceph/client.foo.keyring' |
1400 | + ]) |