Merge lp:~openstack-charmers/charm-helpers/to_upstream into lp:charm-helpers

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
Reviewer Review Type Date Requested Status
Charm Helper Maintainers Pending
Review via email: mp+189838@code.launchpad.net

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 SubordinateConfigContexts, 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+ ])

Subscribers

People subscribed via source and target branches