Merge lp:~corey.bryant/charms/trusty/nova-compute/amulet-basic into lp:~openstack-charmers-archive/charms/trusty/nova-compute/next

Proposed by Corey Bryant
Status: Merged
Merged at revision: 70
Proposed branch: lp:~corey.bryant/charms/trusty/nova-compute/amulet-basic
Merge into: lp:~openstack-charmers-archive/charms/trusty/nova-compute/next
Diff against target: 2192 lines (+1664/-88)
31 files modified
Makefile (+12/-4)
charm-helpers-hooks.yaml (+12/-0)
charm-helpers-tests.yaml (+5/-0)
charm-helpers.yaml (+0/-12)
hooks/charmhelpers/contrib/hahelpers/cluster.py (+1/-0)
hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+57/-0)
hooks/charmhelpers/contrib/openstack/amulet/utils.py (+253/-0)
hooks/charmhelpers/contrib/openstack/context.py (+45/-13)
hooks/charmhelpers/contrib/openstack/neutron.py (+14/-0)
hooks/charmhelpers/contrib/openstack/templating.py (+22/-23)
hooks/charmhelpers/contrib/openstack/utils.py (+5/-2)
hooks/charmhelpers/contrib/storage/linux/ceph.py (+1/-1)
hooks/charmhelpers/contrib/storage/linux/utils.py (+1/-0)
hooks/charmhelpers/core/fstab.py (+116/-0)
hooks/charmhelpers/core/hookenv.py (+5/-4)
hooks/charmhelpers/core/host.py (+28/-12)
hooks/charmhelpers/fetch/__init__.py (+24/-16)
hooks/charmhelpers/fetch/bzrurl.py (+2/-1)
tests/00-setup (+10/-0)
tests/10-basic-precise-essex (+9/-0)
tests/11-basic-precise-folsom (+17/-0)
tests/12-basic-precise-grizzly (+11/-0)
tests/13-basic-precise-havana (+11/-0)
tests/14-basic-precise-icehouse (+11/-0)
tests/15-basic-trusty-icehouse (+9/-0)
tests/README (+47/-0)
tests/basic_deployment.py (+406/-0)
tests/charmhelpers/contrib/amulet/deployment.py (+63/-0)
tests/charmhelpers/contrib/amulet/utils.py (+157/-0)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+57/-0)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+253/-0)
To merge this branch: bzr merge lp:~corey.bryant/charms/trusty/nova-compute/amulet-basic
Reviewer Review Type Date Requested Status
James Page Approve
Review via email: mp+226496@code.launchpad.net
To post a comment you must log in.
Revision history for this message
James Page (james-page) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2014-05-21 10:14:58 +0000
3+++ Makefile 2014-07-11 17:06:18 +0000
4@@ -2,15 +2,23 @@
5 PYTHON := /usr/bin/env python
6
7 lint:
8- @flake8 --exclude hooks/charmhelpers hooks unit_tests
9+ @flake8 --exclude hooks/charmhelpers hooks unit_tests tests
10 @charm proof
11
12+unit_test:
13+ @echo Starting unit tests...
14+ @$(PYTHON) /usr/bin/nosetests --nologcapture --with-coverage unit_tests
15+
16 test:
17- @echo Starting tests...
18- @$(PYTHON) /usr/bin/nosetests --nologcapture --with-coverage unit_tests
19+ @echo Starting Amulet tests...
20+ # coreycb note: The -v should only be temporary until Amulet sends
21+ # raise_status() messages to stderr:
22+ # https://bugs.launchpad.net/amulet/+bug/1320357
23+ @juju test -v -p AMULET_HTTP_PROXY
24
25 sync:
26- @charm-helper-sync -c charm-helpers.yaml
27+ @charm-helper-sync -c charm-helpers-hooks.yaml
28+ @charm-helper-sync -c charm-helpers-tests.yaml
29
30 publish: lint test
31 bzr push lp:charms/nova-compute
32
33=== added file 'charm-helpers-hooks.yaml'
34--- charm-helpers-hooks.yaml 1970-01-01 00:00:00 +0000
35+++ charm-helpers-hooks.yaml 2014-07-11 17:06:18 +0000
36@@ -0,0 +1,12 @@
37+branch: lp:charm-helpers
38+destination: hooks/charmhelpers
39+include:
40+ - core
41+ - fetch
42+ - contrib.openstack|inc=*
43+ - contrib.storage
44+ - contrib.hahelpers:
45+ - apache
46+ - cluster
47+ - contrib.network.ovs
48+ - payload.execd
49
50=== added file 'charm-helpers-tests.yaml'
51--- charm-helpers-tests.yaml 1970-01-01 00:00:00 +0000
52+++ charm-helpers-tests.yaml 2014-07-11 17:06:18 +0000
53@@ -0,0 +1,5 @@
54+branch: lp:charm-helpers
55+destination: tests/charmhelpers
56+include:
57+ - contrib.amulet
58+ - contrib.openstack.amulet
59
60=== removed file 'charm-helpers.yaml'
61--- charm-helpers.yaml 2014-05-10 02:08:40 +0000
62+++ charm-helpers.yaml 1970-01-01 00:00:00 +0000
63@@ -1,12 +0,0 @@
64-branch: lp:charm-helpers
65-destination: hooks/charmhelpers
66-include:
67- - core
68- - fetch
69- - contrib.openstack|inc=*
70- - contrib.storage
71- - contrib.hahelpers:
72- - apache
73- - cluster
74- - contrib.network.ovs
75- - payload.execd
76
77=== modified file 'hooks/charmhelpers/contrib/hahelpers/cluster.py'
78--- hooks/charmhelpers/contrib/hahelpers/cluster.py 2014-04-04 16:45:38 +0000
79+++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2014-07-11 17:06:18 +0000
80@@ -170,6 +170,7 @@
81
82 :configs : OSTemplateRenderer: A config tempating object to inspect for
83 a complete https context.
84+
85 :vip_setting: str: Setting in charm config that specifies
86 VIP address.
87 '''
88
89=== added directory 'hooks/charmhelpers/contrib/openstack/amulet'
90=== added file 'hooks/charmhelpers/contrib/openstack/amulet/__init__.py'
91=== added file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
92--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 1970-01-01 00:00:00 +0000
93+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2014-07-11 17:06:18 +0000
94@@ -0,0 +1,57 @@
95+from charmhelpers.contrib.amulet.deployment import (
96+ AmuletDeployment
97+)
98+
99+
100+class OpenStackAmuletDeployment(AmuletDeployment):
101+ """This class inherits from AmuletDeployment and has additional support
102+ that is specifically for use by OpenStack charms."""
103+
104+ def __init__(self, series, openstack=None, source=None):
105+ """Initialize the deployment environment."""
106+ super(OpenStackAmuletDeployment, self).__init__(series)
107+ self.openstack = openstack
108+ self.source = source
109+
110+ def _add_services(self, this_service, other_services):
111+ """Add services to the deployment and set openstack-origin."""
112+ super(OpenStackAmuletDeployment, self)._add_services(this_service,
113+ other_services)
114+ name = 0
115+ services = other_services
116+ services.append(this_service)
117+ use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph']
118+
119+ if self.openstack:
120+ for svc in services:
121+ charm_name = self._get_charm_name(svc[name])
122+ if charm_name not in use_source:
123+ config = {'openstack-origin': self.openstack}
124+ self.d.configure(svc[name], config)
125+
126+ if self.source:
127+ for svc in services:
128+ charm_name = self._get_charm_name(svc[name])
129+ if charm_name in use_source:
130+ config = {'source': self.source}
131+ self.d.configure(svc[name], config)
132+
133+ def _configure_services(self, configs):
134+ """Configure all of the services."""
135+ for service, config in configs.iteritems():
136+ self.d.configure(service, config)
137+
138+ def _get_openstack_release(self):
139+ """Return an integer representing the enum value of the openstack
140+ release."""
141+ self.precise_essex, self.precise_folsom, self.precise_grizzly, \
142+ self.precise_havana, self.precise_icehouse, \
143+ self.trusty_icehouse = range(6)
144+ releases = {
145+ ('precise', None): self.precise_essex,
146+ ('precise', 'cloud:precise-folsom'): self.precise_folsom,
147+ ('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
148+ ('precise', 'cloud:precise-havana'): self.precise_havana,
149+ ('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
150+ ('trusty', None): self.trusty_icehouse}
151+ return releases[(self.series, self.openstack)]
152
153=== added file 'hooks/charmhelpers/contrib/openstack/amulet/utils.py'
154--- hooks/charmhelpers/contrib/openstack/amulet/utils.py 1970-01-01 00:00:00 +0000
155+++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2014-07-11 17:06:18 +0000
156@@ -0,0 +1,253 @@
157+import logging
158+import os
159+import time
160+import urllib
161+
162+import glanceclient.v1.client as glance_client
163+import keystoneclient.v2_0 as keystone_client
164+import novaclient.v1_1.client as nova_client
165+
166+from charmhelpers.contrib.amulet.utils import (
167+ AmuletUtils
168+)
169+
170+DEBUG = logging.DEBUG
171+ERROR = logging.ERROR
172+
173+
174+class OpenStackAmuletUtils(AmuletUtils):
175+ """This class inherits from AmuletUtils and has additional support
176+ that is specifically for use by OpenStack charms."""
177+
178+ def __init__(self, log_level=ERROR):
179+ """Initialize the deployment environment."""
180+ super(OpenStackAmuletUtils, self).__init__(log_level)
181+
182+ def validate_endpoint_data(self, endpoints, admin_port, internal_port,
183+ public_port, expected):
184+ """Validate actual endpoint data vs expected endpoint data. The ports
185+ are used to find the matching endpoint."""
186+ found = False
187+ for ep in endpoints:
188+ self.log.debug('endpoint: {}'.format(repr(ep)))
189+ if admin_port in ep.adminurl and internal_port in ep.internalurl \
190+ and public_port in ep.publicurl:
191+ found = True
192+ actual = {'id': ep.id,
193+ 'region': ep.region,
194+ 'adminurl': ep.adminurl,
195+ 'internalurl': ep.internalurl,
196+ 'publicurl': ep.publicurl,
197+ 'service_id': ep.service_id}
198+ ret = self._validate_dict_data(expected, actual)
199+ if ret:
200+ return 'unexpected endpoint data - {}'.format(ret)
201+
202+ if not found:
203+ return 'endpoint not found'
204+
205+ def validate_svc_catalog_endpoint_data(self, expected, actual):
206+ """Validate a list of actual service catalog endpoints vs a list of
207+ expected service catalog endpoints."""
208+ self.log.debug('actual: {}'.format(repr(actual)))
209+ for k, v in expected.iteritems():
210+ if k in actual:
211+ ret = self._validate_dict_data(expected[k][0], actual[k][0])
212+ if ret:
213+ return self.endpoint_error(k, ret)
214+ else:
215+ return "endpoint {} does not exist".format(k)
216+ return ret
217+
218+ def validate_tenant_data(self, expected, actual):
219+ """Validate a list of actual tenant data vs list of expected tenant
220+ data."""
221+ self.log.debug('actual: {}'.format(repr(actual)))
222+ for e in expected:
223+ found = False
224+ for act in actual:
225+ a = {'enabled': act.enabled, 'description': act.description,
226+ 'name': act.name, 'id': act.id}
227+ if e['name'] == a['name']:
228+ found = True
229+ ret = self._validate_dict_data(e, a)
230+ if ret:
231+ return "unexpected tenant data - {}".format(ret)
232+ if not found:
233+ return "tenant {} does not exist".format(e['name'])
234+ return ret
235+
236+ def validate_role_data(self, expected, actual):
237+ """Validate a list of actual role data vs a list of expected role
238+ data."""
239+ self.log.debug('actual: {}'.format(repr(actual)))
240+ for e in expected:
241+ found = False
242+ for act in actual:
243+ a = {'name': act.name, 'id': act.id}
244+ if e['name'] == a['name']:
245+ found = True
246+ ret = self._validate_dict_data(e, a)
247+ if ret:
248+ return "unexpected role data - {}".format(ret)
249+ if not found:
250+ return "role {} does not exist".format(e['name'])
251+ return ret
252+
253+ def validate_user_data(self, expected, actual):
254+ """Validate a list of actual user data vs a list of expected user
255+ data."""
256+ self.log.debug('actual: {}'.format(repr(actual)))
257+ for e in expected:
258+ found = False
259+ for act in actual:
260+ a = {'enabled': act.enabled, 'name': act.name,
261+ 'email': act.email, 'tenantId': act.tenantId,
262+ 'id': act.id}
263+ if e['name'] == a['name']:
264+ found = True
265+ ret = self._validate_dict_data(e, a)
266+ if ret:
267+ return "unexpected user data - {}".format(ret)
268+ if not found:
269+ return "user {} does not exist".format(e['name'])
270+ return ret
271+
272+ def validate_flavor_data(self, expected, actual):
273+ """Validate a list of actual flavors vs a list of expected flavors."""
274+ self.log.debug('actual: {}'.format(repr(actual)))
275+ act = [a.name for a in actual]
276+ return self._validate_list_data(expected, act)
277+
278+ def tenant_exists(self, keystone, tenant):
279+ """Return True if tenant exists"""
280+ return tenant in [t.name for t in keystone.tenants.list()]
281+
282+ def authenticate_keystone_admin(self, keystone_sentry, user, password,
283+ tenant):
284+ """Authenticates admin user with the keystone admin endpoint."""
285+ service_ip = \
286+ keystone_sentry.relation('shared-db',
287+ 'mysql:shared-db')['private-address']
288+ ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
289+ return keystone_client.Client(username=user, password=password,
290+ tenant_name=tenant, auth_url=ep)
291+
292+ def authenticate_keystone_user(self, keystone, user, password, tenant):
293+ """Authenticates a regular user with the keystone public endpoint."""
294+ ep = keystone.service_catalog.url_for(service_type='identity',
295+ endpoint_type='publicURL')
296+ return keystone_client.Client(username=user, password=password,
297+ tenant_name=tenant, auth_url=ep)
298+
299+ def authenticate_glance_admin(self, keystone):
300+ """Authenticates admin user with glance."""
301+ ep = keystone.service_catalog.url_for(service_type='image',
302+ endpoint_type='adminURL')
303+ return glance_client.Client(ep, token=keystone.auth_token)
304+
305+ def authenticate_nova_user(self, keystone, user, password, tenant):
306+ """Authenticates a regular user with nova-api."""
307+ ep = keystone.service_catalog.url_for(service_type='identity',
308+ endpoint_type='publicURL')
309+ return nova_client.Client(username=user, api_key=password,
310+ project_id=tenant, auth_url=ep)
311+
312+ def create_cirros_image(self, glance, image_name):
313+ """Download the latest cirros image and upload it to glance."""
314+ http_proxy = os.getenv('AMULET_HTTP_PROXY')
315+ self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
316+ if http_proxy:
317+ proxies = {'http': http_proxy}
318+ opener = urllib.FancyURLopener(proxies)
319+ else:
320+ opener = urllib.FancyURLopener()
321+
322+ f = opener.open("http://download.cirros-cloud.net/version/released")
323+ version = f.read().strip()
324+ cirros_img = "tests/cirros-{}-x86_64-disk.img".format(version)
325+
326+ if not os.path.exists(cirros_img):
327+ cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
328+ version, cirros_img)
329+ opener.retrieve(cirros_url, cirros_img)
330+ f.close()
331+
332+ with open(cirros_img) as f:
333+ image = glance.images.create(name=image_name, is_public=True,
334+ disk_format='qcow2',
335+ container_format='bare', data=f)
336+ count = 1
337+ status = image.status
338+ while status != 'active' and count < 10:
339+ time.sleep(3)
340+ image = glance.images.get(image.id)
341+ status = image.status
342+ self.log.debug('image status: {}'.format(status))
343+ count += 1
344+
345+ if status != 'active':
346+ self.log.error('image creation timed out')
347+ return None
348+
349+ return image
350+
351+ def delete_image(self, glance, image):
352+ """Delete the specified image."""
353+ num_before = len(list(glance.images.list()))
354+ glance.images.delete(image)
355+
356+ count = 1
357+ num_after = len(list(glance.images.list()))
358+ while num_after != (num_before - 1) and count < 10:
359+ time.sleep(3)
360+ num_after = len(list(glance.images.list()))
361+ self.log.debug('number of images: {}'.format(num_after))
362+ count += 1
363+
364+ if num_after != (num_before - 1):
365+ self.log.error('image deletion timed out')
366+ return False
367+
368+ return True
369+
370+ def create_instance(self, nova, image_name, instance_name, flavor):
371+ """Create the specified instance."""
372+ image = nova.images.find(name=image_name)
373+ flavor = nova.flavors.find(name=flavor)
374+ instance = nova.servers.create(name=instance_name, image=image,
375+ flavor=flavor)
376+
377+ count = 1
378+ status = instance.status
379+ while status != 'ACTIVE' and count < 60:
380+ time.sleep(3)
381+ instance = nova.servers.get(instance.id)
382+ status = instance.status
383+ self.log.debug('instance status: {}'.format(status))
384+ count += 1
385+
386+ if status != 'ACTIVE':
387+ self.log.error('instance creation timed out')
388+ return None
389+
390+ return instance
391+
392+ def delete_instance(self, nova, instance):
393+ """Delete the specified instance."""
394+ num_before = len(list(nova.servers.list()))
395+ nova.servers.delete(instance)
396+
397+ count = 1
398+ num_after = len(list(nova.servers.list()))
399+ while num_after != (num_before - 1) and count < 10:
400+ time.sleep(3)
401+ num_after = len(list(nova.servers.list()))
402+ self.log.debug('number of instances: {}'.format(num_after))
403+ count += 1
404+
405+ if num_after != (num_before - 1):
406+ self.log.error('instance deletion timed out')
407+ return False
408+
409+ return True
410
411=== modified file 'hooks/charmhelpers/contrib/openstack/context.py'
412--- hooks/charmhelpers/contrib/openstack/context.py 2014-05-21 10:31:46 +0000
413+++ hooks/charmhelpers/contrib/openstack/context.py 2014-07-11 17:06:18 +0000
414@@ -243,23 +243,31 @@
415
416
417 class AMQPContext(OSContextGenerator):
418- interfaces = ['amqp']
419
420- def __init__(self, ssl_dir=None):
421+ def __init__(self, ssl_dir=None, rel_name='amqp', relation_prefix=None):
422 self.ssl_dir = ssl_dir
423+ self.rel_name = rel_name
424+ self.relation_prefix = relation_prefix
425+ self.interfaces = [rel_name]
426
427 def __call__(self):
428 log('Generating template context for amqp')
429 conf = config()
430+ user_setting = 'rabbit-user'
431+ vhost_setting = 'rabbit-vhost'
432+ if self.relation_prefix:
433+ user_setting = self.relation_prefix + '-rabbit-user'
434+ vhost_setting = self.relation_prefix + '-rabbit-vhost'
435+
436 try:
437- username = conf['rabbit-user']
438- vhost = conf['rabbit-vhost']
439+ username = conf[user_setting]
440+ vhost = conf[vhost_setting]
441 except KeyError as e:
442 log('Could not generate shared_db context. '
443 'Missing required charm config options: %s.' % e)
444 raise OSContextError
445 ctxt = {}
446- for rid in relation_ids('amqp'):
447+ for rid in relation_ids(self.rel_name):
448 ha_vip_only = False
449 for unit in related_units(rid):
450 if relation_get('clustered', rid=rid, unit=unit):
451@@ -418,12 +426,13 @@
452 """
453 Generates a context for an apache vhost configuration that configures
454 HTTPS reverse proxying for one or many endpoints. Generated context
455- looks something like:
456- {
457- 'namespace': 'cinder',
458- 'private_address': 'iscsi.mycinderhost.com',
459- 'endpoints': [(8776, 8766), (8777, 8767)]
460- }
461+ looks something like::
462+
463+ {
464+ 'namespace': 'cinder',
465+ 'private_address': 'iscsi.mycinderhost.com',
466+ 'endpoints': [(8776, 8766), (8777, 8767)]
467+ }
468
469 The endpoints list consists of a tuples mapping external ports
470 to internal ports.
471@@ -541,6 +550,26 @@
472
473 return nvp_ctxt
474
475+ def n1kv_ctxt(self):
476+ driver = neutron_plugin_attribute(self.plugin, 'driver',
477+ self.network_manager)
478+ n1kv_config = neutron_plugin_attribute(self.plugin, 'config',
479+ self.network_manager)
480+ n1kv_ctxt = {
481+ 'core_plugin': driver,
482+ 'neutron_plugin': 'n1kv',
483+ 'neutron_security_groups': self.neutron_security_groups,
484+ 'local_ip': unit_private_ip(),
485+ 'config': n1kv_config,
486+ 'vsm_ip': config('n1kv-vsm-ip'),
487+ 'vsm_username': config('n1kv-vsm-username'),
488+ 'vsm_password': config('n1kv-vsm-password'),
489+ 'restrict_policy_profiles': config(
490+ 'n1kv_restrict_policy_profiles'),
491+ }
492+
493+ return n1kv_ctxt
494+
495 def neutron_ctxt(self):
496 if https():
497 proto = 'https'
498@@ -572,6 +601,8 @@
499 ctxt.update(self.ovs_ctxt())
500 elif self.plugin in ['nvp', 'nsx']:
501 ctxt.update(self.nvp_ctxt())
502+ elif self.plugin == 'n1kv':
503+ ctxt.update(self.n1kv_ctxt())
504
505 alchemy_flags = config('neutron-alchemy-flags')
506 if alchemy_flags:
507@@ -611,7 +642,7 @@
508 The subordinate interface allows subordinates to export their
509 configuration requirements to the principle for multiple config
510 files and multiple serivces. Ie, a subordinate that has interfaces
511- to both glance and nova may export to following yaml blob as json:
512+ to both glance and nova may export to following yaml blob as json::
513
514 glance:
515 /etc/glance/glance-api.conf:
516@@ -630,7 +661,8 @@
517
518 It is then up to the principle charms to subscribe this context to
519 the service+config file it is interestd in. Configuration data will
520- be available in the template context, in glance's case, as:
521+ be available in the template context, in glance's case, as::
522+
523 ctxt = {
524 ... other context ...
525 'subordinate_config': {
526
527=== modified file 'hooks/charmhelpers/contrib/openstack/neutron.py'
528--- hooks/charmhelpers/contrib/openstack/neutron.py 2014-05-21 10:31:46 +0000
529+++ hooks/charmhelpers/contrib/openstack/neutron.py 2014-07-11 17:06:18 +0000
530@@ -128,6 +128,20 @@
531 'server_packages': ['neutron-server',
532 'neutron-plugin-vmware'],
533 'server_services': ['neutron-server']
534+ },
535+ 'n1kv': {
536+ 'config': '/etc/neutron/plugins/cisco/cisco_plugins.ini',
537+ 'driver': 'neutron.plugins.cisco.network_plugin.PluginV2',
538+ 'contexts': [
539+ context.SharedDBContext(user=config('neutron-database-user'),
540+ database=config('neutron-database'),
541+ relation_prefix='neutron',
542+ ssl_dir=NEUTRON_CONF_DIR)],
543+ 'services': [],
544+ 'packages': [['neutron-plugin-cisco']],
545+ 'server_packages': ['neutron-server',
546+ 'neutron-plugin-cisco'],
547+ 'server_services': ['neutron-server']
548 }
549 }
550 if release >= 'icehouse':
551
552=== modified file 'hooks/charmhelpers/contrib/openstack/templating.py'
553--- hooks/charmhelpers/contrib/openstack/templating.py 2014-02-24 19:32:20 +0000
554+++ hooks/charmhelpers/contrib/openstack/templating.py 2014-07-11 17:06:18 +0000
555@@ -30,17 +30,17 @@
556 loading dir.
557
558 A charm may also ship a templates dir with this module
559- and it will be appended to the bottom of the search list, eg:
560- hooks/charmhelpers/contrib/openstack/templates.
561-
562- :param templates_dir: str: Base template directory containing release
563- sub-directories.
564- :param os_release : str: OpenStack release codename to construct template
565- loader.
566-
567- :returns : jinja2.ChoiceLoader constructed with a list of
568- jinja2.FilesystemLoaders, ordered in descending
569- order by OpenStack release.
570+ and it will be appended to the bottom of the search list, eg::
571+
572+ hooks/charmhelpers/contrib/openstack/templates
573+
574+ :param templates_dir (str): Base template directory containing release
575+ sub-directories.
576+ :param os_release (str): OpenStack release codename to construct template
577+ loader.
578+ :returns: jinja2.ChoiceLoader constructed with a list of
579+ jinja2.FilesystemLoaders, ordered in descending
580+ order by OpenStack release.
581 """
582 tmpl_dirs = [(rel, os.path.join(templates_dir, rel))
583 for rel in OPENSTACK_CODENAMES.itervalues()]
584@@ -111,7 +111,8 @@
585 and ease the burden of managing config templates across multiple OpenStack
586 releases.
587
588- Basic usage:
589+ Basic usage::
590+
591 # import some common context generates from charmhelpers
592 from charmhelpers.contrib.openstack import context
593
594@@ -131,21 +132,19 @@
595 # write out all registered configs
596 configs.write_all()
597
598- Details:
599+ **OpenStack Releases and template loading**
600
601- OpenStack Releases and template loading
602- ---------------------------------------
603 When the object is instantiated, it is associated with a specific OS
604 release. This dictates how the template loader will be constructed.
605
606 The constructed loader attempts to load the template from several places
607 in the following order:
608- - from the most recent OS release-specific template dir (if one exists)
609- - the base templates_dir
610- - a template directory shipped in the charm with this helper file.
611-
612-
613- For the example above, '/tmp/templates' contains the following structure:
614+ - from the most recent OS release-specific template dir (if one exists)
615+ - the base templates_dir
616+ - a template directory shipped in the charm with this helper file.
617+
618+ For the example above, '/tmp/templates' contains the following structure::
619+
620 /tmp/templates/nova.conf
621 /tmp/templates/api-paste.ini
622 /tmp/templates/grizzly/api-paste.ini
623@@ -169,8 +168,8 @@
624 $CHARM/hooks/charmhelpers/contrib/openstack/templates. This allows
625 us to ship common templates (haproxy, apache) with the helpers.
626
627- Context generators
628- ---------------------------------------
629+ **Context generators**
630+
631 Context generators are used to generate template contexts during hook
632 execution. Doing so may require inspecting service relations, charm
633 config, etc. When registered, a config file is associated with a list
634
635=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
636--- hooks/charmhelpers/contrib/openstack/utils.py 2014-06-16 14:47:04 +0000
637+++ hooks/charmhelpers/contrib/openstack/utils.py 2014-07-11 17:06:18 +0000
638@@ -3,7 +3,6 @@
639 # Common python helper functions used for OpenStack charms.
640 from collections import OrderedDict
641
642-import apt_pkg as apt
643 import subprocess
644 import os
645 import socket
646@@ -85,6 +84,8 @@
647 '''Derive OpenStack release codename from a given installation source.'''
648 ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
649 rel = ''
650+ if src is None:
651+ return rel
652 if src in ['distro', 'distro-proposed']:
653 try:
654 rel = UBUNTU_OPENSTACK_RELEASE[ubuntu_rel]
655@@ -132,6 +133,7 @@
656
657 def get_os_codename_package(package, fatal=True):
658 '''Derive OpenStack release codename from an installed package.'''
659+ import apt_pkg as apt
660 apt.init()
661
662 # Tell apt to build an in-memory cache to prevent race conditions (if
663@@ -189,7 +191,7 @@
664 for version, cname in vers_map.iteritems():
665 if cname == codename:
666 return version
667- #e = "Could not determine OpenStack version for package: %s" % pkg
668+ # e = "Could not determine OpenStack version for package: %s" % pkg
669 # error_out(e)
670
671
672@@ -325,6 +327,7 @@
673
674 """
675
676+ import apt_pkg as apt
677 src = config('openstack-origin')
678 cur_vers = get_os_version_package(package)
679 available_vers = get_os_version_install_source(src)
680
681=== modified file 'hooks/charmhelpers/contrib/storage/linux/ceph.py'
682--- hooks/charmhelpers/contrib/storage/linux/ceph.py 2014-04-04 16:45:38 +0000
683+++ hooks/charmhelpers/contrib/storage/linux/ceph.py 2014-07-11 17:06:18 +0000
684@@ -303,7 +303,7 @@
685 blk_device, fstype, system_services=[]):
686 """
687 NOTE: This function must only be called from a single service unit for
688- the same rbd_img otherwise data loss will occur.
689+ the same rbd_img otherwise data loss will occur.
690
691 Ensures given pool and RBD image exists, is mapped to a block device,
692 and the device is formatted and mounted at the given mount_point.
693
694=== modified file 'hooks/charmhelpers/contrib/storage/linux/utils.py'
695--- hooks/charmhelpers/contrib/storage/linux/utils.py 2014-05-21 10:31:46 +0000
696+++ hooks/charmhelpers/contrib/storage/linux/utils.py 2014-07-11 17:06:18 +0000
697@@ -37,6 +37,7 @@
698 check_call(['dd', 'if=/dev/zero', 'of=%s' % (block_device),
699 'bs=512', 'count=100', 'seek=%s' % (gpt_end)])
700
701+
702 def is_device_mounted(device):
703 '''Given a device path, return True if that device is mounted, and False
704 if it isn't.
705
706=== added file 'hooks/charmhelpers/core/fstab.py'
707--- hooks/charmhelpers/core/fstab.py 1970-01-01 00:00:00 +0000
708+++ hooks/charmhelpers/core/fstab.py 2014-07-11 17:06:18 +0000
709@@ -0,0 +1,116 @@
710+#!/usr/bin/env python
711+# -*- coding: utf-8 -*-
712+
713+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
714+
715+import os
716+
717+
718+class Fstab(file):
719+ """This class extends file in order to implement a file reader/writer
720+ for file `/etc/fstab`
721+ """
722+
723+ class Entry(object):
724+ """Entry class represents a non-comment line on the `/etc/fstab` file
725+ """
726+ def __init__(self, device, mountpoint, filesystem,
727+ options, d=0, p=0):
728+ self.device = device
729+ self.mountpoint = mountpoint
730+ self.filesystem = filesystem
731+
732+ if not options:
733+ options = "defaults"
734+
735+ self.options = options
736+ self.d = d
737+ self.p = p
738+
739+ def __eq__(self, o):
740+ return str(self) == str(o)
741+
742+ def __str__(self):
743+ return "{} {} {} {} {} {}".format(self.device,
744+ self.mountpoint,
745+ self.filesystem,
746+ self.options,
747+ self.d,
748+ self.p)
749+
750+ DEFAULT_PATH = os.path.join(os.path.sep, 'etc', 'fstab')
751+
752+ def __init__(self, path=None):
753+ if path:
754+ self._path = path
755+ else:
756+ self._path = self.DEFAULT_PATH
757+ file.__init__(self, self._path, 'r+')
758+
759+ def _hydrate_entry(self, line):
760+ # NOTE: use split with no arguments to split on any
761+ # whitespace including tabs
762+ return Fstab.Entry(*filter(
763+ lambda x: x not in ('', None),
764+ line.strip("\n").split()))
765+
766+ @property
767+ def entries(self):
768+ self.seek(0)
769+ for line in self.readlines():
770+ try:
771+ if not line.startswith("#"):
772+ yield self._hydrate_entry(line)
773+ except ValueError:
774+ pass
775+
776+ def get_entry_by_attr(self, attr, value):
777+ for entry in self.entries:
778+ e_attr = getattr(entry, attr)
779+ if e_attr == value:
780+ return entry
781+ return None
782+
783+ def add_entry(self, entry):
784+ if self.get_entry_by_attr('device', entry.device):
785+ return False
786+
787+ self.write(str(entry) + '\n')
788+ self.truncate()
789+ return entry
790+
791+ def remove_entry(self, entry):
792+ self.seek(0)
793+
794+ lines = self.readlines()
795+
796+ found = False
797+ for index, line in enumerate(lines):
798+ if not line.startswith("#"):
799+ if self._hydrate_entry(line) == entry:
800+ found = True
801+ break
802+
803+ if not found:
804+ return False
805+
806+ lines.remove(line)
807+
808+ self.seek(0)
809+ self.write(''.join(lines))
810+ self.truncate()
811+ return True
812+
813+ @classmethod
814+ def remove_by_mountpoint(cls, mountpoint, path=None):
815+ fstab = cls(path=path)
816+ entry = fstab.get_entry_by_attr('mountpoint', mountpoint)
817+ if entry:
818+ return fstab.remove_entry(entry)
819+ return False
820+
821+ @classmethod
822+ def add(cls, device, mountpoint, filesystem, options=None, path=None):
823+ return cls(path=path).add_entry(Fstab.Entry(device,
824+ mountpoint, filesystem,
825+ options=options))
826
827=== modified file 'hooks/charmhelpers/core/hookenv.py'
828--- hooks/charmhelpers/core/hookenv.py 2014-05-19 11:41:02 +0000
829+++ hooks/charmhelpers/core/hookenv.py 2014-07-11 17:06:18 +0000
830@@ -25,7 +25,7 @@
831 def cached(func):
832 """Cache return values for multiple executions of func + args
833
834- For example:
835+ For example::
836
837 @cached
838 def unit_get(attribute):
839@@ -445,18 +445,19 @@
840 class Hooks(object):
841 """A convenient handler for hook functions.
842
843- Example:
844+ Example::
845+
846 hooks = Hooks()
847
848 # register a hook, taking its name from the function name
849 @hooks.hook()
850 def install():
851- ...
852+ pass # your code here
853
854 # register a hook, providing a custom hook name
855 @hooks.hook("config-changed")
856 def config_changed():
857- ...
858+ pass # your code here
859
860 if __name__ == "__main__":
861 # execute a hook based on the name the program is called by
862
863=== modified file 'hooks/charmhelpers/core/host.py'
864--- hooks/charmhelpers/core/host.py 2014-05-19 11:41:02 +0000
865+++ hooks/charmhelpers/core/host.py 2014-07-11 17:06:18 +0000
866@@ -12,11 +12,11 @@
867 import string
868 import subprocess
869 import hashlib
870-import apt_pkg
871
872 from collections import OrderedDict
873
874 from hookenv import log
875+from fstab import Fstab
876
877
878 def service_start(service_name):
879@@ -35,7 +35,8 @@
880
881
882 def service_reload(service_name, restart_on_failure=False):
883- """Reload a system service, optionally falling back to restart if reload fails"""
884+ """Reload a system service, optionally falling back to restart if
885+ reload fails"""
886 service_result = service('reload', service_name)
887 if not service_result and restart_on_failure:
888 service_result = service('restart', service_name)
889@@ -144,7 +145,19 @@
890 target.write(content)
891
892
893-def mount(device, mountpoint, options=None, persist=False):
894+def fstab_remove(mp):
895+ """Remove the given mountpoint entry from /etc/fstab
896+ """
897+ return Fstab.remove_by_mountpoint(mp)
898+
899+
900+def fstab_add(dev, mp, fs, options=None):
901+ """Adds the given device entry to the /etc/fstab file
902+ """
903+ return Fstab.add(dev, mp, fs, options=options)
904+
905+
906+def mount(device, mountpoint, options=None, persist=False, filesystem="ext3"):
907 """Mount a filesystem at a particular mountpoint"""
908 cmd_args = ['mount']
909 if options is not None:
910@@ -155,9 +168,9 @@
911 except subprocess.CalledProcessError, e:
912 log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
913 return False
914+
915 if persist:
916- # TODO: update fstab
917- pass
918+ return fstab_add(device, mountpoint, filesystem, options=options)
919 return True
920
921
922@@ -169,9 +182,9 @@
923 except subprocess.CalledProcessError, e:
924 log('Error unmounting {}\n{}'.format(mountpoint, e.output))
925 return False
926+
927 if persist:
928- # TODO: update fstab
929- pass
930+ return fstab_remove(mountpoint)
931 return True
932
933
934@@ -198,13 +211,13 @@
935 def restart_on_change(restart_map, stopstart=False):
936 """Restart services based on configuration files changing
937
938- This function is used a decorator, for example
939+ This function is used a decorator, for example::
940
941 @restart_on_change({
942 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
943 })
944 def ceph_client_changed():
945- ...
946+ pass # your code here
947
948 In this example, the cinder-api and cinder-volume services
949 would be restarted if /etc/ceph/ceph.conf is changed by the
950@@ -300,10 +313,13 @@
951
952 def cmp_pkgrevno(package, revno, pkgcache=None):
953 '''Compare supplied revno with the revno of the installed package
954- 1 => Installed revno is greater than supplied arg
955- 0 => Installed revno is the same as supplied arg
956- -1 => Installed revno is less than supplied arg
957+
958+ * 1 => Installed revno is greater than supplied arg
959+ * 0 => Installed revno is the same as supplied arg
960+ * -1 => Installed revno is less than supplied arg
961+
962 '''
963+ import apt_pkg
964 if not pkgcache:
965 apt_pkg.init()
966 pkgcache = apt_pkg.Cache()
967
968=== modified file 'hooks/charmhelpers/fetch/__init__.py'
969--- hooks/charmhelpers/fetch/__init__.py 2014-06-04 13:06:25 +0000
970+++ hooks/charmhelpers/fetch/__init__.py 2014-07-11 17:06:18 +0000
971@@ -13,7 +13,6 @@
972 config,
973 log,
974 )
975-import apt_pkg
976 import os
977
978
979@@ -117,6 +116,7 @@
980
981 def filter_installed_packages(packages):
982 """Returns a list of packages that require installation"""
983+ import apt_pkg
984 apt_pkg.init()
985
986 # Tell apt to build an in-memory cache to prevent race conditions (if
987@@ -235,31 +235,39 @@
988 sources_var='install_sources',
989 keys_var='install_keys'):
990 """
991- Configure multiple sources from charm configuration
992+ Configure multiple sources from charm configuration.
993+
994+ The lists are encoded as yaml fragments in the configuration.
995+ The frament needs to be included as a string.
996
997 Example config:
998- install_sources:
999+ install_sources: |
1000 - "ppa:foo"
1001 - "http://example.com/repo precise main"
1002- install_keys:
1003+ install_keys: |
1004 - null
1005 - "a1b2c3d4"
1006
1007 Note that 'null' (a.k.a. None) should not be quoted.
1008 """
1009- sources = safe_load(config(sources_var))
1010- keys = config(keys_var)
1011- if keys is not None:
1012- keys = safe_load(keys)
1013- if isinstance(sources, basestring) and (
1014- keys is None or isinstance(keys, basestring)):
1015- add_source(sources, keys)
1016+ sources = safe_load((config(sources_var) or '').strip()) or []
1017+ keys = safe_load((config(keys_var) or '').strip()) or None
1018+
1019+ if isinstance(sources, basestring):
1020+ sources = [sources]
1021+
1022+ if keys is None:
1023+ for source in sources:
1024+ add_source(source, None)
1025 else:
1026- if not len(sources) == len(keys):
1027- msg = 'Install sources and keys lists are different lengths'
1028- raise SourceConfigError(msg)
1029- for src_num in range(len(sources)):
1030- add_source(sources[src_num], keys[src_num])
1031+ if isinstance(keys, basestring):
1032+ keys = [keys]
1033+
1034+ if len(sources) != len(keys):
1035+ raise SourceConfigError(
1036+ 'Install sources and keys lists are different lengths')
1037+ for source, key in zip(sources, keys):
1038+ add_source(source, key)
1039 if update:
1040 apt_update(fatal=True)
1041
1042
1043=== modified file 'hooks/charmhelpers/fetch/bzrurl.py'
1044--- hooks/charmhelpers/fetch/bzrurl.py 2013-11-06 03:50:44 +0000
1045+++ hooks/charmhelpers/fetch/bzrurl.py 2014-07-11 17:06:18 +0000
1046@@ -39,7 +39,8 @@
1047 def install(self, source):
1048 url_parts = self.parse_url(source)
1049 branch_name = url_parts.path.strip("/").split("/")[-1]
1050- dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched", branch_name)
1051+ dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
1052+ branch_name)
1053 if not os.path.exists(dest_dir):
1054 mkdir(dest_dir, perms=0755)
1055 try:
1056
1057=== added directory 'tests'
1058=== added file 'tests/00-setup'
1059--- tests/00-setup 1970-01-01 00:00:00 +0000
1060+++ tests/00-setup 2014-07-11 17:06:18 +0000
1061@@ -0,0 +1,10 @@
1062+#!/bin/bash
1063+
1064+set -ex
1065+
1066+sudo add-apt-repository --yes ppa:juju/stable
1067+sudo apt-get update --yes
1068+sudo apt-get install --yes python-amulet
1069+sudo apt-get install --yes python-glanceclient
1070+sudo apt-get install --yes python-keystoneclient
1071+sudo apt-get install --yes python-novaclient
1072
1073=== added file 'tests/10-basic-precise-essex'
1074--- tests/10-basic-precise-essex 1970-01-01 00:00:00 +0000
1075+++ tests/10-basic-precise-essex 2014-07-11 17:06:18 +0000
1076@@ -0,0 +1,9 @@
1077+#!/usr/bin/python
1078+
1079+"""Amulet tests on a basic nova compute deployment on precise-essex."""
1080+
1081+from basic_deployment import NovaBasicDeployment
1082+
1083+if __name__ == '__main__':
1084+ deployment = NovaBasicDeployment(series='precise')
1085+ deployment.run_tests()
1086
1087=== added file 'tests/11-basic-precise-folsom'
1088--- tests/11-basic-precise-folsom 1970-01-01 00:00:00 +0000
1089+++ tests/11-basic-precise-folsom 2014-07-11 17:06:18 +0000
1090@@ -0,0 +1,17 @@
1091+#!/usr/bin/python
1092+
1093+"""Amulet tests on a basic nova compute deployment on precise-folsom."""
1094+
1095+import amulet
1096+from basic_deployment import NovaBasicDeployment
1097+
1098+if __name__ == '__main__':
1099+ # NOTE(coreycb): Skipping failing test until resolved. 'nova-manage db sync'
1100+ # fails in shared-db-relation-changed (only fails on folsom)
1101+ message = "Skipping failing test until resolved"
1102+ amulet.raise_status(amulet.SKIP, msg=message)
1103+
1104+ deployment = NovaBasicDeployment(series='precise',
1105+ openstack='cloud:precise-folsom',
1106+ source='cloud:precise-updates/folsom')
1107+ deployment.run_tests()
1108
1109=== added file 'tests/12-basic-precise-grizzly'
1110--- tests/12-basic-precise-grizzly 1970-01-01 00:00:00 +0000
1111+++ tests/12-basic-precise-grizzly 2014-07-11 17:06:18 +0000
1112@@ -0,0 +1,11 @@
1113+#!/usr/bin/python
1114+
1115+"""Amulet tests on a basic nova compute deployment on precise-grizzly."""
1116+
1117+from basic_deployment import NovaBasicDeployment
1118+
1119+if __name__ == '__main__':
1120+ deployment = NovaBasicDeployment(series='precise',
1121+ openstack='cloud:precise-grizzly',
1122+ source='cloud:precise-updates/grizzly')
1123+ deployment.run_tests()
1124
1125=== added file 'tests/13-basic-precise-havana'
1126--- tests/13-basic-precise-havana 1970-01-01 00:00:00 +0000
1127+++ tests/13-basic-precise-havana 2014-07-11 17:06:18 +0000
1128@@ -0,0 +1,11 @@
1129+#!/usr/bin/python
1130+
1131+"""Amulet tests on a basic nova compute deployment on precise-havana."""
1132+
1133+from basic_deployment import NovaBasicDeployment
1134+
1135+if __name__ == '__main__':
1136+ deployment = NovaBasicDeployment(series='precise',
1137+ openstack='cloud:precise-havana',
1138+ source='cloud:precise-updates/havana')
1139+ deployment.run_tests()
1140
1141=== added file 'tests/14-basic-precise-icehouse'
1142--- tests/14-basic-precise-icehouse 1970-01-01 00:00:00 +0000
1143+++ tests/14-basic-precise-icehouse 2014-07-11 17:06:18 +0000
1144@@ -0,0 +1,11 @@
1145+#!/usr/bin/python
1146+
1147+"""Amulet tests on a basic nova compute deployment on precise-icehouse."""
1148+
1149+from basic_deployment import NovaBasicDeployment
1150+
1151+if __name__ == '__main__':
1152+ deployment = NovaBasicDeployment(series='precise',
1153+ openstack='cloud:precise-icehouse',
1154+ source='cloud:precise-updates/icehouse')
1155+ deployment.run_tests()
1156
1157=== added file 'tests/15-basic-trusty-icehouse'
1158--- tests/15-basic-trusty-icehouse 1970-01-01 00:00:00 +0000
1159+++ tests/15-basic-trusty-icehouse 2014-07-11 17:06:18 +0000
1160@@ -0,0 +1,9 @@
1161+#!/usr/bin/python
1162+
1163+"""Amulet tests on a basic nova compute deployment on trusty-icehouse."""
1164+
1165+from basic_deployment import NovaBasicDeployment
1166+
1167+if __name__ == '__main__':
1168+ deployment = NovaBasicDeployment(series='trusty')
1169+ deployment.run_tests()
1170
1171=== added file 'tests/README'
1172--- tests/README 1970-01-01 00:00:00 +0000
1173+++ tests/README 2014-07-11 17:06:18 +0000
1174@@ -0,0 +1,47 @@
1175+This directory provides Amulet tests that focus on verification of nova-compute
1176+deployments.
1177+
1178+If you use a web proxy server to access the web, you'll need to set the
1179+AMULET_HTTP_PROXY environment variable to the http URL of the proxy server.
1180+
1181+The following examples demonstrate different ways that tests can be executed.
1182+All examples are run from the charm's root directory.
1183+
1184+ * To run all tests (starting with 00-setup):
1185+
1186+ make test
1187+
1188+ * To run a specific test module (or modules):
1189+
1190+ juju test -v -p AMULET_HTTP_PROXY 15-basic-trusty-icehouse
1191+
1192+ * To run a specific test module (or modules), and keep the environment
1193+ deployed after a failure:
1194+
1195+ juju test --set-e -v -p AMULET_HTTP_PROXY 15-basic-trusty-icehouse
1196+
1197+ * To re-run a test module against an already deployed environment (one
1198+ that was deployed by a previous call to 'juju test --set-e'):
1199+
1200+ ./tests/15-basic-trusty-icehouse
1201+
1202+For debugging and test development purposes, all code should be idempotent.
1203+In other words, the code should have the ability to be re-run without changing
1204+the results beyond the initial run. This enables editing and re-running of a
1205+test module against an already deployed environment, as described above.
1206+
1207+Manual debugging tips:
1208+
1209+ * Set the following env vars before using the OpenStack CLI as admin:
1210+ export OS_AUTH_URL=http://`juju-deployer -f keystone 2>&1 | tail -n 1`:5000/v2.0
1211+ export OS_TENANT_NAME=admin
1212+ export OS_USERNAME=admin
1213+ export OS_PASSWORD=openstack
1214+ export OS_REGION_NAME=RegionOne
1215+
1216+ * Set the following env vars before using the OpenStack CLI as demoUser:
1217+ export OS_AUTH_URL=http://`juju-deployer -f keystone 2>&1 | tail -n 1`:5000/v2.0
1218+ export OS_TENANT_NAME=demoTenant
1219+ export OS_USERNAME=demoUser
1220+ export OS_PASSWORD=password
1221+ export OS_REGION_NAME=RegionOne
1222
1223=== added file 'tests/basic_deployment.py'
1224--- tests/basic_deployment.py 1970-01-01 00:00:00 +0000
1225+++ tests/basic_deployment.py 2014-07-11 17:06:18 +0000
1226@@ -0,0 +1,406 @@
1227+#!/usr/bin/python
1228+
1229+import amulet
1230+
1231+from charmhelpers.contrib.openstack.amulet.deployment import (
1232+ OpenStackAmuletDeployment
1233+)
1234+
1235+from charmhelpers.contrib.openstack.amulet.utils import (
1236+ OpenStackAmuletUtils,
1237+ DEBUG, # flake8: noqa
1238+ ERROR
1239+)
1240+
1241+# Use DEBUG to turn on debug logging
1242+u = OpenStackAmuletUtils(ERROR)
1243+
1244+
1245+class NovaBasicDeployment(OpenStackAmuletDeployment):
1246+ """Amulet tests on a basic nova compute deployment."""
1247+
1248+ def __init__(self, series=None, openstack=None, source=None):
1249+ """Deploy the entire test environment."""
1250+ super(NovaBasicDeployment, self).__init__(series, openstack, source)
1251+ self._add_services()
1252+ self._add_relations()
1253+ self._configure_services()
1254+ self._deploy()
1255+ self._initialize_tests()
1256+
1257+ def _add_services(self):
1258+ """Add the service that we're testing, including the number of units,
1259+ where nova-compute is local, and the other charms are from
1260+ the charm store."""
1261+ this_service = ('nova-compute', 1)
1262+ other_services = [('mysql', 1), ('rabbitmq-server', 1),
1263+ ('nova-cloud-controller', 1), ('keystone', 1),
1264+ ('glance', 1)]
1265+ super(NovaBasicDeployment, self)._add_services(this_service,
1266+ other_services)
1267+
1268+ def _add_relations(self):
1269+ """Add all of the relations for the services."""
1270+ relations = {
1271+ 'nova-compute:image-service': 'glance:image-service',
1272+ 'nova-compute:shared-db': 'mysql:shared-db',
1273+ 'nova-compute:amqp': 'rabbitmq-server:amqp',
1274+ 'nova-cloud-controller:shared-db': 'mysql:shared-db',
1275+ 'nova-cloud-controller:identity-service': 'keystone:identity-service',
1276+ 'nova-cloud-controller:amqp': 'rabbitmq-server:amqp',
1277+ 'nova-cloud-controller:cloud-compute': 'nova-compute:cloud-compute',
1278+ 'nova-cloud-controller:image-service': 'glance:image-service',
1279+ 'keystone:shared-db': 'mysql:shared-db',
1280+ 'glance:identity-service': 'keystone:identity-service',
1281+ 'glance:shared-db': 'mysql:shared-db',
1282+ 'glance:amqp': 'rabbitmq-server:amqp'
1283+ }
1284+ super(NovaBasicDeployment, self)._add_relations(relations)
1285+
1286+ def _configure_services(self):
1287+ """Configure all of the services."""
1288+ nova_config = {'config-flags': 'auto_assign_floating_ip=False',
1289+ 'enable-live-migration': 'False'}
1290+ keystone_config = {'admin-password': 'openstack',
1291+ 'admin-token': 'ubuntutesting'}
1292+ configs = {'nova-compute': nova_config, 'keystone': keystone_config}
1293+ super(NovaBasicDeployment, self)._configure_services(configs)
1294+
1295+ def _initialize_tests(self):
1296+ """Perform final initialization before tests get run."""
1297+ # Access the sentries for inspecting service units
1298+ self.mysql_sentry = self.d.sentry.unit['mysql/0']
1299+ self.keystone_sentry = self.d.sentry.unit['keystone/0']
1300+ self.rabbitmq_sentry = self.d.sentry.unit['rabbitmq-server/0']
1301+ self.nova_compute_sentry = self.d.sentry.unit['nova-compute/0']
1302+ self.nova_cc_sentry = self.d.sentry.unit['nova-cloud-controller/0']
1303+ self.glance_sentry = self.d.sentry.unit['glance/0']
1304+
1305+ # Authenticate admin with keystone
1306+ self.keystone = u.authenticate_keystone_admin(self.keystone_sentry,
1307+ user='admin',
1308+ password='openstack',
1309+ tenant='admin')
1310+
1311+ # Authenticate admin with glance endpoint
1312+ self.glance = u.authenticate_glance_admin(self.keystone)
1313+
1314+ # Create a demo tenant/role/user
1315+ self.demo_tenant = 'demoTenant'
1316+ self.demo_role = 'demoRole'
1317+ self.demo_user = 'demoUser'
1318+ if not u.tenant_exists(self.keystone, self.demo_tenant):
1319+ tenant = self.keystone.tenants.create(tenant_name=self.demo_tenant,
1320+ description='demo tenant',
1321+ enabled=True)
1322+ self.keystone.roles.create(name=self.demo_role)
1323+ self.keystone.users.create(name=self.demo_user,
1324+ password='password',
1325+ tenant_id=tenant.id,
1326+ email='demo@demo.com')
1327+
1328+ # Authenticate demo user with keystone
1329+ self.keystone_demo = \
1330+ u.authenticate_keystone_user(self.keystone, user=self.demo_user,
1331+ password='password',
1332+ tenant=self.demo_tenant)
1333+
1334+ # Authenticate demo user with nova-api
1335+ self.nova_demo = u.authenticate_nova_user(self.keystone,
1336+ user=self.demo_user,
1337+ password='password',
1338+ tenant=self.demo_tenant)
1339+
1340+ def test_services(self):
1341+ """Verify the expected services are running on the corresponding
1342+ service units."""
1343+ commands = {
1344+ self.mysql_sentry: ['status mysql'],
1345+ self.rabbitmq_sentry: ['sudo service rabbitmq-server status'],
1346+ self.nova_compute_sentry: ['status nova-compute',
1347+ 'status nova-network',
1348+ 'status nova-api'],
1349+ self.nova_cc_sentry: ['status nova-api-ec2',
1350+ 'status nova-api-os-compute',
1351+ 'status nova-objectstore',
1352+ 'status nova-cert',
1353+ 'status nova-scheduler'],
1354+ self.keystone_sentry: ['status keystone'],
1355+ self.glance_sentry: ['status glance-registry', 'status glance-api']
1356+ }
1357+ if self._get_openstack_release() >= self.precise_grizzly:
1358+ commands[self.nova_cc_sentry] = ['status nova-conductor']
1359+
1360+ ret = u.validate_services(commands)
1361+ if ret:
1362+ amulet.raise_status(amulet.FAIL, msg=ret)
1363+
1364+ def test_service_catalog(self):
1365+ """Verify that the service catalog endpoint data is valid."""
1366+ endpoint_vol = {'adminURL': u.valid_url,
1367+ 'region': 'RegionOne',
1368+ 'publicURL': u.valid_url,
1369+ 'internalURL': u.valid_url}
1370+ endpoint_id = {'adminURL': u.valid_url,
1371+ 'region': 'RegionOne',
1372+ 'publicURL': u.valid_url,
1373+ 'internalURL': u.valid_url}
1374+ if self._get_openstack_release() >= self.precise_folsom:
1375+ endpoint_vol['id'] = u.not_null
1376+ endpoint_id['id'] = u.not_null
1377+ expected = {'s3': [endpoint_vol], 'compute': [endpoint_vol],
1378+ 'ec2': [endpoint_vol], 'identity': [endpoint_id]}
1379+ actual = self.keystone_demo.service_catalog.get_endpoints()
1380+
1381+ ret = u.validate_svc_catalog_endpoint_data(expected, actual)
1382+ if ret:
1383+ amulet.raise_status(amulet.FAIL, msg=ret)
1384+
1385+ def test_openstack_compute_api_endpoint(self):
1386+ """Verify the openstack compute api (osapi) endpoint data."""
1387+ endpoints = self.keystone.endpoints.list()
1388+ admin_port = internal_port = public_port = '8774'
1389+ expected = {'id': u.not_null,
1390+ 'region': 'RegionOne',
1391+ 'adminurl': u.valid_url,
1392+ 'internalurl': u.valid_url,
1393+ 'publicurl': u.valid_url,
1394+ 'service_id': u.not_null}
1395+
1396+ ret = u.validate_endpoint_data(endpoints, admin_port, internal_port,
1397+ public_port, expected)
1398+ if ret:
1399+ message = 'osapi endpoint: {}'.format(ret)
1400+ amulet.raise_status(amulet.FAIL, msg=message)
1401+
1402+ def test_ec2_api_endpoint(self):
1403+ """Verify the EC2 api endpoint data."""
1404+ endpoints = self.keystone.endpoints.list()
1405+ admin_port = internal_port = public_port = '8773'
1406+ expected = {'id': u.not_null,
1407+ 'region': 'RegionOne',
1408+ 'adminurl': u.valid_url,
1409+ 'internalurl': u.valid_url,
1410+ 'publicurl': u.valid_url,
1411+ 'service_id': u.not_null}
1412+
1413+ ret = u.validate_endpoint_data(endpoints, admin_port, internal_port,
1414+ public_port, expected)
1415+ if ret:
1416+ message = 'EC2 endpoint: {}'.format(ret)
1417+ amulet.raise_status(amulet.FAIL, msg=message)
1418+
1419+ def test_s3_api_endpoint(self):
1420+ """Verify the S3 api endpoint data."""
1421+ endpoints = self.keystone.endpoints.list()
1422+ admin_port = internal_port = public_port = '3333'
1423+ expected = {'id': u.not_null,
1424+ 'region': 'RegionOne',
1425+ 'adminurl': u.valid_url,
1426+ 'internalurl': u.valid_url,
1427+ 'publicurl': u.valid_url,
1428+ 'service_id': u.not_null}
1429+
1430+ ret = u.validate_endpoint_data(endpoints, admin_port, internal_port,
1431+ public_port, expected)
1432+ if ret:
1433+ message = 'S3 endpoint: {}'.format(ret)
1434+ amulet.raise_status(amulet.FAIL, msg=message)
1435+
1436+ def test_nova_shared_db_relation(self):
1437+ """Verify the nova-compute to mysql shared-db relation data"""
1438+ unit = self.nova_compute_sentry
1439+ relation = ['shared-db', 'mysql:shared-db']
1440+ expected = {
1441+ 'private-address': u.valid_ip,
1442+ 'nova_database': 'nova',
1443+ 'nova_username': 'nova',
1444+ 'nova_hostname': u.valid_ip
1445+ }
1446+
1447+ ret = u.validate_relation_data(unit, relation, expected)
1448+ if ret:
1449+ message = u.relation_error('nova-compute shared-db', ret)
1450+ amulet.raise_status(amulet.FAIL, msg=message)
1451+
1452+ def test_mysql_shared_db_relation(self):
1453+ """Verify the mysql to nova-compute shared-db relation data"""
1454+ unit = self.mysql_sentry
1455+ relation = ['shared-db', 'nova-compute:shared-db']
1456+ expected = {
1457+ 'private-address': u.valid_ip,
1458+ 'nova_password': u.not_null,
1459+ 'db_host': u.valid_ip
1460+ }
1461+
1462+ ret = u.validate_relation_data(unit, relation, expected)
1463+ if ret:
1464+ message = u.relation_error('mysql shared-db', ret)
1465+ amulet.raise_status(amulet.FAIL, msg=message)
1466+
1467+ def test_nova_amqp_relation(self):
1468+ """Verify the nova-compute to rabbitmq-server amqp relation data"""
1469+ unit = self.nova_compute_sentry
1470+ relation = ['amqp', 'rabbitmq-server:amqp']
1471+ expected = {
1472+ 'username': 'nova',
1473+ 'private-address': u.valid_ip,
1474+ 'vhost': 'openstack'
1475+ }
1476+
1477+ ret = u.validate_relation_data(unit, relation, expected)
1478+ if ret:
1479+ message = u.relation_error('nova-compute amqp', ret)
1480+ amulet.raise_status(amulet.FAIL, msg=message)
1481+
1482+ def test_rabbitmq_amqp_relation(self):
1483+ """Verify the rabbitmq-server to nova-compute amqp relation data"""
1484+ unit = self.rabbitmq_sentry
1485+ relation = ['amqp', 'nova-compute:amqp']
1486+ expected = {
1487+ 'private-address': u.valid_ip,
1488+ 'password': u.not_null,
1489+ 'hostname': u.valid_ip
1490+ }
1491+
1492+ ret = u.validate_relation_data(unit, relation, expected)
1493+ if ret:
1494+ message = u.relation_error('rabbitmq amqp', ret)
1495+ amulet.raise_status(amulet.FAIL, msg=message)
1496+
1497+ def test_nova_cloud_compute_relation(self):
1498+ """Verify the nova-compute to nova-cc cloud-compute relation data"""
1499+ unit = self.nova_compute_sentry
1500+ relation = ['cloud-compute', 'nova-cloud-controller:cloud-compute']
1501+ expected = {
1502+ 'private-address': u.valid_ip,
1503+ }
1504+
1505+ ret = u.validate_relation_data(unit, relation, expected)
1506+ if ret:
1507+ message = u.relation_error('nova-compute cloud-compute', ret)
1508+ amulet.raise_status(amulet.FAIL, msg=message)
1509+
1510+ def test_nova_cc_cloud_compute_relation(self):
1511+ """Verify the nova-cc to nova-compute cloud-compute relation data"""
1512+ unit = self.nova_cc_sentry
1513+ relation = ['cloud-compute', 'nova-compute:cloud-compute']
1514+ expected = {
1515+ 'volume_service': 'cinder',
1516+ 'network_manager': 'flatdhcpmanager',
1517+ 'ec2_host': u.valid_ip,
1518+ 'private-address': u.valid_ip,
1519+ 'restart_trigger': u.not_null
1520+ }
1521+ if self._get_openstack_release() == self.precise_essex:
1522+ expected['volume_service'] = 'nova-volume'
1523+
1524+ ret = u.validate_relation_data(unit, relation, expected)
1525+ if ret:
1526+ message = u.relation_error('nova-cc cloud-compute', ret)
1527+ amulet.raise_status(amulet.FAIL, msg=message)
1528+
1529+ def test_restart_on_config_change(self):
1530+ """Verify that the specified services are restarted when the config
1531+ is changed."""
1532+ # NOTE(coreycb): Skipping failing test on essex until resolved.
1533+ # config-flags don't take effect on essex.
1534+ if self._get_openstack_release() == self.precise_essex:
1535+ u.log.error("Skipping failing test until resolved")
1536+ return
1537+
1538+ services = ['nova-compute', 'nova-api', 'nova-network']
1539+ self.d.configure('nova-compute', {'config-flags': 'verbose=False'})
1540+
1541+ time = 20
1542+ for s in services:
1543+ if not u.service_restarted(self.nova_compute_sentry, s,
1544+ '/etc/nova/nova.conf', sleep_time=time):
1545+ msg = "service {} didn't restart after config change".format(s)
1546+ amulet.raise_status(amulet.FAIL, msg=msg)
1547+ time = 0
1548+
1549+ self.d.configure('nova-compute', {'config-flags': 'verbose=True'})
1550+
1551+ def test_nova_config(self):
1552+ """Verify the data in the nova config file."""
1553+ # NOTE(coreycb): Currently no way to test on essex because config file
1554+ # has no section headers.
1555+ if self._get_openstack_release() == self.precise_essex:
1556+ return
1557+
1558+ unit = self.nova_compute_sentry
1559+ conf = '/etc/nova/nova.conf'
1560+ rabbitmq_relation = self.rabbitmq_sentry.relation('amqp',
1561+ 'nova-compute:amqp')
1562+ glance_relation = self.glance_sentry.relation('image-service',
1563+ 'nova-compute:image-service')
1564+ mysql_relation = self.mysql_sentry.relation('shared-db',
1565+ 'nova-compute:shared-db')
1566+ db_uri = "mysql://{}:{}@{}/{}".format('nova',
1567+ mysql_relation['nova_password'],
1568+ mysql_relation['db_host'],
1569+ 'nova')
1570+
1571+ expected = {'dhcpbridge_flagfile': '/etc/nova/nova.conf',
1572+ 'dhcpbridge': '/usr/bin/nova-dhcpbridge',
1573+ 'logdir': '/var/log/nova',
1574+ 'state_path': '/var/lib/nova',
1575+ 'lock_path': '/var/lock/nova',
1576+ 'force_dhcp_release': 'True',
1577+ 'libvirt_use_virtio_for_bridges': 'True',
1578+ 'verbose': 'True',
1579+ 'use_syslog': 'False',
1580+ 'ec2_private_dns_show_ip': 'True',
1581+ 'api_paste_config': '/etc/nova/api-paste.ini',
1582+ 'enabled_apis': 'ec2,osapi_compute,metadata',
1583+ 'auth_strategy': 'keystone',
1584+ 'compute_driver': 'libvirt.LibvirtDriver',
1585+ 'sql_connection': db_uri,
1586+ 'rabbit_userid': 'nova',
1587+ 'rabbit_virtual_host': 'openstack',
1588+ 'rabbit_password': rabbitmq_relation['password'],
1589+ 'rabbit_host': rabbitmq_relation['hostname'],
1590+ 'glance_api_servers': glance_relation['glance-api-server'],
1591+ 'flat_interface': 'eth1',
1592+ 'network_manager': 'nova.network.manager.FlatDHCPManager',
1593+ 'volume_api_class': 'nova.volume.cinder.API',
1594+ 'verbose': 'True'}
1595+
1596+ ret = u.validate_config_data(unit, conf, 'DEFAULT', expected)
1597+ if ret:
1598+ message = "nova config error: {}".format(ret)
1599+ amulet.raise_status(amulet.FAIL, msg=message)
1600+
1601+ def test_image_instance_create(self):
1602+ """Create an image/instance, verify they exist, and delete them."""
1603+ # NOTE(coreycb): Skipping failing test on essex until resolved. essex
1604+ # nova API calls are getting "Malformed request url (HTTP
1605+ # 400)".
1606+ if self._get_openstack_release() == self.precise_essex:
1607+ u.log.error("Skipping failing test until resolved")
1608+ return
1609+
1610+ image = u.create_cirros_image(self.glance, "cirros-image")
1611+ if not image:
1612+ amulet.raise_status(amulet.FAIL, msg="Image create failed")
1613+
1614+ instance = u.create_instance(self.nova_demo, "cirros-image", "cirros",
1615+ "m1.tiny")
1616+ if not instance:
1617+ amulet.raise_status(amulet.FAIL, msg="Instance create failed")
1618+
1619+ found = False
1620+ for instance in self.nova_demo.servers.list():
1621+ if instance.name == 'cirros':
1622+ found = True
1623+ if instance.status != 'ACTIVE':
1624+ msg = "cirros instance is not active"
1625+ amulet.raise_status(amulet.FAIL, msg=message)
1626+
1627+ if not found:
1628+ message = "nova cirros instance does not exist"
1629+ amulet.raise_status(amulet.FAIL, msg=message)
1630+
1631+ u.delete_image(self.glance, image)
1632+ u.delete_instance(self.nova_demo, instance)
1633
1634=== added directory 'tests/charmhelpers'
1635=== added file 'tests/charmhelpers/__init__.py'
1636=== added directory 'tests/charmhelpers/contrib'
1637=== added file 'tests/charmhelpers/contrib/__init__.py'
1638=== added directory 'tests/charmhelpers/contrib/amulet'
1639=== added file 'tests/charmhelpers/contrib/amulet/__init__.py'
1640=== added file 'tests/charmhelpers/contrib/amulet/deployment.py'
1641--- tests/charmhelpers/contrib/amulet/deployment.py 1970-01-01 00:00:00 +0000
1642+++ tests/charmhelpers/contrib/amulet/deployment.py 2014-07-11 17:06:18 +0000
1643@@ -0,0 +1,63 @@
1644+import amulet
1645+import re
1646+
1647+
1648+class AmuletDeployment(object):
1649+ """This class provides generic Amulet deployment and test runner
1650+ methods."""
1651+
1652+ def __init__(self, series):
1653+ """Initialize the deployment environment."""
1654+ self.series = series
1655+ self.d = amulet.Deployment(series=self.series)
1656+
1657+ def _get_charm_name(self, service_name):
1658+ """Gets the charm name from the service name. Unique service names can
1659+ be specified with a '-service#' suffix (e.g. mysql-service1)."""
1660+ if re.match(r"^.*-service\d{1,3}$", service_name):
1661+ charm_name = re.sub('\-service\d{1,3}$', '', service_name)
1662+ else:
1663+ charm_name = service_name
1664+ return charm_name
1665+
1666+ def _add_services(self, this_service, other_services):
1667+ """Add services to the deployment where this_service is the local charm
1668+ that we're focused on testing and other_services are the other
1669+ charms that come from the charm store."""
1670+ name, units = range(2)
1671+
1672+ charm_name = self._get_charm_name(this_service[name])
1673+ self.d.add(this_service[name],
1674+ units=this_service[units])
1675+
1676+ for svc in other_services:
1677+ charm_name = self._get_charm_name(svc[name])
1678+ self.d.add(svc[name],
1679+ charm='cs:{}/{}'.format(self.series, charm_name),
1680+ units=svc[units])
1681+
1682+ def _add_relations(self, relations):
1683+ """Add all of the relations for the services."""
1684+ for k, v in relations.iteritems():
1685+ self.d.relate(k, v)
1686+
1687+ def _configure_services(self, configs):
1688+ """Configure all of the services."""
1689+ for service, config in configs.iteritems():
1690+ self.d.configure(service, config)
1691+
1692+ def _deploy(self):
1693+ """Deploy environment and wait for all hooks to finish executing."""
1694+ try:
1695+ self.d.setup()
1696+ self.d.sentry.wait()
1697+ except amulet.helpers.TimeoutError:
1698+ amulet.raise_status(amulet.FAIL, msg="Deployment timed out")
1699+ except:
1700+ raise
1701+
1702+ def run_tests(self):
1703+ """Run all of the methods that are prefixed with 'test_'."""
1704+ for test in dir(self):
1705+ if test.startswith('test_'):
1706+ getattr(self, test)()
1707
1708=== added file 'tests/charmhelpers/contrib/amulet/utils.py'
1709--- tests/charmhelpers/contrib/amulet/utils.py 1970-01-01 00:00:00 +0000
1710+++ tests/charmhelpers/contrib/amulet/utils.py 2014-07-11 17:06:18 +0000
1711@@ -0,0 +1,157 @@
1712+import ConfigParser
1713+import io
1714+import logging
1715+import re
1716+import sys
1717+from time import sleep
1718+
1719+
1720+class AmuletUtils(object):
1721+ """This class provides common utility functions that are used by Amulet
1722+ tests."""
1723+
1724+ def __init__(self, log_level=logging.ERROR):
1725+ self.log = self.get_logger(level=log_level)
1726+
1727+ def get_logger(self, name="amulet-logger", level=logging.DEBUG):
1728+ """Get a logger object that will log to stdout."""
1729+ log = logging
1730+ logger = log.getLogger(name)
1731+ fmt = \
1732+ log.Formatter("%(asctime)s %(funcName)s %(levelname)s: %(message)s")
1733+
1734+ handler = log.StreamHandler(stream=sys.stdout)
1735+ handler.setLevel(level)
1736+ handler.setFormatter(fmt)
1737+
1738+ logger.addHandler(handler)
1739+ logger.setLevel(level)
1740+
1741+ return logger
1742+
1743+ def valid_ip(self, ip):
1744+ if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip):
1745+ return True
1746+ else:
1747+ return False
1748+
1749+ def valid_url(self, url):
1750+ p = re.compile(
1751+ r'^(?:http|ftp)s?://'
1752+ r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # flake8: noqa
1753+ r'localhost|'
1754+ r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
1755+ r'(?::\d+)?'
1756+ r'(?:/?|[/?]\S+)$',
1757+ re.IGNORECASE)
1758+ if p.match(url):
1759+ return True
1760+ else:
1761+ return False
1762+
1763+ def validate_services(self, commands):
1764+ """Verify the specified services are running on the corresponding
1765+ service units."""
1766+ for k, v in commands.iteritems():
1767+ for cmd in v:
1768+ output, code = k.run(cmd)
1769+ if code != 0:
1770+ return "command `{}` returned {}".format(cmd, str(code))
1771+ return None
1772+
1773+ def _get_config(self, unit, filename):
1774+ """Get a ConfigParser object for parsing a unit's config file."""
1775+ file_contents = unit.file_contents(filename)
1776+ config = ConfigParser.ConfigParser()
1777+ config.readfp(io.StringIO(file_contents))
1778+ return config
1779+
1780+ def validate_config_data(self, sentry_unit, config_file, section, expected):
1781+ """Verify that the specified section of the config file contains
1782+ the expected option key:value pairs."""
1783+ config = self._get_config(sentry_unit, config_file)
1784+
1785+ if section != 'DEFAULT' and not config.has_section(section):
1786+ return "section [{}] does not exist".format(section)
1787+
1788+ for k in expected.keys():
1789+ if not config.has_option(section, k):
1790+ return "section [{}] is missing option {}".format(section, k)
1791+ if config.get(section, k) != expected[k]:
1792+ return "section [{}] {}:{} != expected {}:{}".format(section,
1793+ k, config.get(section, k), k, expected[k])
1794+ return None
1795+
1796+ def _validate_dict_data(self, expected, actual):
1797+ """Compare expected dictionary data vs actual dictionary data.
1798+ The values in the 'expected' dictionary can be strings, bools, ints,
1799+ longs, or can be a function that evaluate a variable and returns a
1800+ bool."""
1801+ for k, v in expected.iteritems():
1802+ if k in actual:
1803+ if isinstance(v, basestring) or \
1804+ isinstance(v, bool) or \
1805+ isinstance(v, (int, long)):
1806+ if v != actual[k]:
1807+ return "{}:{}".format(k, actual[k])
1808+ elif not v(actual[k]):
1809+ return "{}:{}".format(k, actual[k])
1810+ else:
1811+ return "key '{}' does not exist".format(k)
1812+ return None
1813+
1814+ def validate_relation_data(self, sentry_unit, relation, expected):
1815+ """Validate actual relation data based on expected relation data."""
1816+ actual = sentry_unit.relation(relation[0], relation[1])
1817+ self.log.debug('actual: {}'.format(repr(actual)))
1818+ return self._validate_dict_data(expected, actual)
1819+
1820+ def _validate_list_data(self, expected, actual):
1821+ """Compare expected list vs actual list data."""
1822+ for e in expected:
1823+ if e not in actual:
1824+ return "expected item {} not found in actual list".format(e)
1825+ return None
1826+
1827+ def not_null(self, string):
1828+ if string != None:
1829+ return True
1830+ else:
1831+ return False
1832+
1833+ def _get_file_mtime(self, sentry_unit, filename):
1834+ """Get last modification time of file."""
1835+ return sentry_unit.file_stat(filename)['mtime']
1836+
1837+ def _get_dir_mtime(self, sentry_unit, directory):
1838+ """Get last modification time of directory."""
1839+ return sentry_unit.directory_stat(directory)['mtime']
1840+
1841+ def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False):
1842+ """Determine start time of the process based on the last modification
1843+ time of the /proc/pid directory. If pgrep_full is True, the process
1844+ name is matched against the full command line."""
1845+ if pgrep_full:
1846+ cmd = 'pgrep -o -f {}'.format(service)
1847+ else:
1848+ cmd = 'pgrep -o {}'.format(service)
1849+ proc_dir = '/proc/{}'.format(sentry_unit.run(cmd)[0].strip())
1850+ return self._get_dir_mtime(sentry_unit, proc_dir)
1851+
1852+ def service_restarted(self, sentry_unit, service, filename,
1853+ pgrep_full=False, sleep_time=20):
1854+ """Compare a service's start time vs a file's last modification time
1855+ (such as a config file for that service) to determine if the service
1856+ has been restarted."""
1857+ sleep(sleep_time)
1858+ if self._get_proc_start_time(sentry_unit, service, pgrep_full) >= \
1859+ self._get_file_mtime(sentry_unit, filename):
1860+ return True
1861+ else:
1862+ return False
1863+
1864+ def relation_error(self, name, data):
1865+ return 'unexpected relation data in {} - {}'.format(name, data)
1866+
1867+ def endpoint_error(self, name, data):
1868+ return 'unexpected endpoint data in {} - {}'.format(name, data)
1869
1870=== added directory 'tests/charmhelpers/contrib/openstack'
1871=== added file 'tests/charmhelpers/contrib/openstack/__init__.py'
1872=== added directory 'tests/charmhelpers/contrib/openstack/amulet'
1873=== added file 'tests/charmhelpers/contrib/openstack/amulet/__init__.py'
1874=== added file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
1875--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 1970-01-01 00:00:00 +0000
1876+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2014-07-11 17:06:18 +0000
1877@@ -0,0 +1,57 @@
1878+from charmhelpers.contrib.amulet.deployment import (
1879+ AmuletDeployment
1880+)
1881+
1882+
1883+class OpenStackAmuletDeployment(AmuletDeployment):
1884+ """This class inherits from AmuletDeployment and has additional support
1885+ that is specifically for use by OpenStack charms."""
1886+
1887+ def __init__(self, series, openstack=None, source=None):
1888+ """Initialize the deployment environment."""
1889+ super(OpenStackAmuletDeployment, self).__init__(series)
1890+ self.openstack = openstack
1891+ self.source = source
1892+
1893+ def _add_services(self, this_service, other_services):
1894+ """Add services to the deployment and set openstack-origin."""
1895+ super(OpenStackAmuletDeployment, self)._add_services(this_service,
1896+ other_services)
1897+ name = 0
1898+ services = other_services
1899+ services.append(this_service)
1900+ use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph']
1901+
1902+ if self.openstack:
1903+ for svc in services:
1904+ charm_name = self._get_charm_name(svc[name])
1905+ if charm_name not in use_source:
1906+ config = {'openstack-origin': self.openstack}
1907+ self.d.configure(svc[name], config)
1908+
1909+ if self.source:
1910+ for svc in services:
1911+ charm_name = self._get_charm_name(svc[name])
1912+ if charm_name in use_source:
1913+ config = {'source': self.source}
1914+ self.d.configure(svc[name], config)
1915+
1916+ def _configure_services(self, configs):
1917+ """Configure all of the services."""
1918+ for service, config in configs.iteritems():
1919+ self.d.configure(service, config)
1920+
1921+ def _get_openstack_release(self):
1922+ """Return an integer representing the enum value of the openstack
1923+ release."""
1924+ self.precise_essex, self.precise_folsom, self.precise_grizzly, \
1925+ self.precise_havana, self.precise_icehouse, \
1926+ self.trusty_icehouse = range(6)
1927+ releases = {
1928+ ('precise', None): self.precise_essex,
1929+ ('precise', 'cloud:precise-folsom'): self.precise_folsom,
1930+ ('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
1931+ ('precise', 'cloud:precise-havana'): self.precise_havana,
1932+ ('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
1933+ ('trusty', None): self.trusty_icehouse}
1934+ return releases[(self.series, self.openstack)]
1935
1936=== added file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
1937--- tests/charmhelpers/contrib/openstack/amulet/utils.py 1970-01-01 00:00:00 +0000
1938+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2014-07-11 17:06:18 +0000
1939@@ -0,0 +1,253 @@
1940+import logging
1941+import os
1942+import time
1943+import urllib
1944+
1945+import glanceclient.v1.client as glance_client
1946+import keystoneclient.v2_0 as keystone_client
1947+import novaclient.v1_1.client as nova_client
1948+
1949+from charmhelpers.contrib.amulet.utils import (
1950+ AmuletUtils
1951+)
1952+
1953+DEBUG = logging.DEBUG
1954+ERROR = logging.ERROR
1955+
1956+
1957+class OpenStackAmuletUtils(AmuletUtils):
1958+ """This class inherits from AmuletUtils and has additional support
1959+ that is specifically for use by OpenStack charms."""
1960+
1961+ def __init__(self, log_level=ERROR):
1962+ """Initialize the deployment environment."""
1963+ super(OpenStackAmuletUtils, self).__init__(log_level)
1964+
1965+ def validate_endpoint_data(self, endpoints, admin_port, internal_port,
1966+ public_port, expected):
1967+ """Validate actual endpoint data vs expected endpoint data. The ports
1968+ are used to find the matching endpoint."""
1969+ found = False
1970+ for ep in endpoints:
1971+ self.log.debug('endpoint: {}'.format(repr(ep)))
1972+ if admin_port in ep.adminurl and internal_port in ep.internalurl \
1973+ and public_port in ep.publicurl:
1974+ found = True
1975+ actual = {'id': ep.id,
1976+ 'region': ep.region,
1977+ 'adminurl': ep.adminurl,
1978+ 'internalurl': ep.internalurl,
1979+ 'publicurl': ep.publicurl,
1980+ 'service_id': ep.service_id}
1981+ ret = self._validate_dict_data(expected, actual)
1982+ if ret:
1983+ return 'unexpected endpoint data - {}'.format(ret)
1984+
1985+ if not found:
1986+ return 'endpoint not found'
1987+
1988+ def validate_svc_catalog_endpoint_data(self, expected, actual):
1989+ """Validate a list of actual service catalog endpoints vs a list of
1990+ expected service catalog endpoints."""
1991+ self.log.debug('actual: {}'.format(repr(actual)))
1992+ for k, v in expected.iteritems():
1993+ if k in actual:
1994+ ret = self._validate_dict_data(expected[k][0], actual[k][0])
1995+ if ret:
1996+ return self.endpoint_error(k, ret)
1997+ else:
1998+ return "endpoint {} does not exist".format(k)
1999+ return ret
2000+
2001+ def validate_tenant_data(self, expected, actual):
2002+ """Validate a list of actual tenant data vs list of expected tenant
2003+ data."""
2004+ self.log.debug('actual: {}'.format(repr(actual)))
2005+ for e in expected:
2006+ found = False
2007+ for act in actual:
2008+ a = {'enabled': act.enabled, 'description': act.description,
2009+ 'name': act.name, 'id': act.id}
2010+ if e['name'] == a['name']:
2011+ found = True
2012+ ret = self._validate_dict_data(e, a)
2013+ if ret:
2014+ return "unexpected tenant data - {}".format(ret)
2015+ if not found:
2016+ return "tenant {} does not exist".format(e['name'])
2017+ return ret
2018+
2019+ def validate_role_data(self, expected, actual):
2020+ """Validate a list of actual role data vs a list of expected role
2021+ data."""
2022+ self.log.debug('actual: {}'.format(repr(actual)))
2023+ for e in expected:
2024+ found = False
2025+ for act in actual:
2026+ a = {'name': act.name, 'id': act.id}
2027+ if e['name'] == a['name']:
2028+ found = True
2029+ ret = self._validate_dict_data(e, a)
2030+ if ret:
2031+ return "unexpected role data - {}".format(ret)
2032+ if not found:
2033+ return "role {} does not exist".format(e['name'])
2034+ return ret
2035+
2036+ def validate_user_data(self, expected, actual):
2037+ """Validate a list of actual user data vs a list of expected user
2038+ data."""
2039+ self.log.debug('actual: {}'.format(repr(actual)))
2040+ for e in expected:
2041+ found = False
2042+ for act in actual:
2043+ a = {'enabled': act.enabled, 'name': act.name,
2044+ 'email': act.email, 'tenantId': act.tenantId,
2045+ 'id': act.id}
2046+ if e['name'] == a['name']:
2047+ found = True
2048+ ret = self._validate_dict_data(e, a)
2049+ if ret:
2050+ return "unexpected user data - {}".format(ret)
2051+ if not found:
2052+ return "user {} does not exist".format(e['name'])
2053+ return ret
2054+
2055+ def validate_flavor_data(self, expected, actual):
2056+ """Validate a list of actual flavors vs a list of expected flavors."""
2057+ self.log.debug('actual: {}'.format(repr(actual)))
2058+ act = [a.name for a in actual]
2059+ return self._validate_list_data(expected, act)
2060+
2061+ def tenant_exists(self, keystone, tenant):
2062+ """Return True if tenant exists"""
2063+ return tenant in [t.name for t in keystone.tenants.list()]
2064+
2065+ def authenticate_keystone_admin(self, keystone_sentry, user, password,
2066+ tenant):
2067+ """Authenticates admin user with the keystone admin endpoint."""
2068+ service_ip = \
2069+ keystone_sentry.relation('shared-db',
2070+ 'mysql:shared-db')['private-address']
2071+ ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
2072+ return keystone_client.Client(username=user, password=password,
2073+ tenant_name=tenant, auth_url=ep)
2074+
2075+ def authenticate_keystone_user(self, keystone, user, password, tenant):
2076+ """Authenticates a regular user with the keystone public endpoint."""
2077+ ep = keystone.service_catalog.url_for(service_type='identity',
2078+ endpoint_type='publicURL')
2079+ return keystone_client.Client(username=user, password=password,
2080+ tenant_name=tenant, auth_url=ep)
2081+
2082+ def authenticate_glance_admin(self, keystone):
2083+ """Authenticates admin user with glance."""
2084+ ep = keystone.service_catalog.url_for(service_type='image',
2085+ endpoint_type='adminURL')
2086+ return glance_client.Client(ep, token=keystone.auth_token)
2087+
2088+ def authenticate_nova_user(self, keystone, user, password, tenant):
2089+ """Authenticates a regular user with nova-api."""
2090+ ep = keystone.service_catalog.url_for(service_type='identity',
2091+ endpoint_type='publicURL')
2092+ return nova_client.Client(username=user, api_key=password,
2093+ project_id=tenant, auth_url=ep)
2094+
2095+ def create_cirros_image(self, glance, image_name):
2096+ """Download the latest cirros image and upload it to glance."""
2097+ http_proxy = os.getenv('AMULET_HTTP_PROXY')
2098+ self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
2099+ if http_proxy:
2100+ proxies = {'http': http_proxy}
2101+ opener = urllib.FancyURLopener(proxies)
2102+ else:
2103+ opener = urllib.FancyURLopener()
2104+
2105+ f = opener.open("http://download.cirros-cloud.net/version/released")
2106+ version = f.read().strip()
2107+ cirros_img = "tests/cirros-{}-x86_64-disk.img".format(version)
2108+
2109+ if not os.path.exists(cirros_img):
2110+ cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
2111+ version, cirros_img)
2112+ opener.retrieve(cirros_url, cirros_img)
2113+ f.close()
2114+
2115+ with open(cirros_img) as f:
2116+ image = glance.images.create(name=image_name, is_public=True,
2117+ disk_format='qcow2',
2118+ container_format='bare', data=f)
2119+ count = 1
2120+ status = image.status
2121+ while status != 'active' and count < 10:
2122+ time.sleep(3)
2123+ image = glance.images.get(image.id)
2124+ status = image.status
2125+ self.log.debug('image status: {}'.format(status))
2126+ count += 1
2127+
2128+ if status != 'active':
2129+ self.log.error('image creation timed out')
2130+ return None
2131+
2132+ return image
2133+
2134+ def delete_image(self, glance, image):
2135+ """Delete the specified image."""
2136+ num_before = len(list(glance.images.list()))
2137+ glance.images.delete(image)
2138+
2139+ count = 1
2140+ num_after = len(list(glance.images.list()))
2141+ while num_after != (num_before - 1) and count < 10:
2142+ time.sleep(3)
2143+ num_after = len(list(glance.images.list()))
2144+ self.log.debug('number of images: {}'.format(num_after))
2145+ count += 1
2146+
2147+ if num_after != (num_before - 1):
2148+ self.log.error('image deletion timed out')
2149+ return False
2150+
2151+ return True
2152+
2153+ def create_instance(self, nova, image_name, instance_name, flavor):
2154+ """Create the specified instance."""
2155+ image = nova.images.find(name=image_name)
2156+ flavor = nova.flavors.find(name=flavor)
2157+ instance = nova.servers.create(name=instance_name, image=image,
2158+ flavor=flavor)
2159+
2160+ count = 1
2161+ status = instance.status
2162+ while status != 'ACTIVE' and count < 60:
2163+ time.sleep(3)
2164+ instance = nova.servers.get(instance.id)
2165+ status = instance.status
2166+ self.log.debug('instance status: {}'.format(status))
2167+ count += 1
2168+
2169+ if status != 'ACTIVE':
2170+ self.log.error('instance creation timed out')
2171+ return None
2172+
2173+ return instance
2174+
2175+ def delete_instance(self, nova, instance):
2176+ """Delete the specified instance."""
2177+ num_before = len(list(nova.servers.list()))
2178+ nova.servers.delete(instance)
2179+
2180+ count = 1
2181+ num_after = len(list(nova.servers.list()))
2182+ while num_after != (num_before - 1) and count < 10:
2183+ time.sleep(3)
2184+ num_after = len(list(nova.servers.list()))
2185+ self.log.debug('number of instances: {}'.format(num_after))
2186+ count += 1
2187+
2188+ if num_after != (num_before - 1):
2189+ self.log.error('instance deletion timed out')
2190+ return False
2191+
2192+ return True

Subscribers

People subscribed via source and target branches