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

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

Subscribers

People subscribed via source and target branches