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
=== modified file 'Makefile'
--- Makefile 2014-05-21 10:08:22 +0000
+++ Makefile 2014-07-11 16:43:39 +0000
@@ -3,15 +3,23 @@
33
4lint:4lint:
5 @flake8 --exclude hooks/charmhelpers hooks5 @flake8 --exclude hooks/charmhelpers hooks
6 @flake8 --exclude hooks/charmhelpers unit_tests6 @flake8 --exclude hooks/charmhelpers unit_tests tests
7 @charm proof7 @charm proof
88
9unit_test:
10 @echo Starting unit tests...
11 @$(PYTHON) /usr/bin/nosetests -v --nologcapture --with-coverage unit_tests
12
9test:13test:
10 @echo Starting tests...14 @echo Starting Amulet tests...
11 @$(PYTHON) /usr/bin/nosetests -v --nologcapture --with-coverage unit_tests15 # coreycb note: The -v should only be temporary until Amulet sends
16 # raise_status() messages to stderr:
17 # https://bugs.launchpad.net/amulet/+bug/1320357
18 @juju test -v -p AMULET_HTTP_PROXY
1219
13sync:20sync:
14 @charm-helper-sync -c charm-helpers.yaml21 @charm-helper-sync -c charm-helpers-hooks.yaml
22 @charm-helper-sync -c charm-helpers-tests.yaml
1523
16publish: lint test24publish: lint test
17 bzr push lp:charms/swift-storage25 bzr push lp:charms/swift-storage
1826
=== added file 'charm-helpers-hooks.yaml'
--- charm-helpers-hooks.yaml 1970-01-01 00:00:00 +0000
+++ charm-helpers-hooks.yaml 2014-07-11 16:43:39 +0000
@@ -0,0 +1,11 @@
1branch: lp:charm-helpers
2destination: hooks/charmhelpers
3include:
4 - core
5 - contrib.openstack|inc=*
6 - contrib.storage
7 - fetch
8 - contrib.hahelpers:
9 - apache
10 - cluster
11 - payload.execd
012
=== added file 'charm-helpers-tests.yaml'
--- charm-helpers-tests.yaml 1970-01-01 00:00:00 +0000
+++ charm-helpers-tests.yaml 2014-07-11 16:43:39 +0000
@@ -0,0 +1,5 @@
1branch: lp:charm-helpers
2destination: tests/charmhelpers
3include:
4 - contrib.amulet
5 - contrib.openstack.amulet
06
=== removed file 'charm-helpers.yaml'
--- charm-helpers.yaml 2014-03-25 17:05:07 +0000
+++ charm-helpers.yaml 1970-01-01 00:00:00 +0000
@@ -1,11 +0,0 @@
1branch: lp:charm-helpers
2destination: hooks/charmhelpers
3include:
4 - core
5 - contrib.openstack|inc=*
6 - contrib.storage
7 - fetch
8 - contrib.hahelpers:
9 - apache
10 - cluster
11 - payload.execd
120
=== modified file 'hooks/charmhelpers/contrib/hahelpers/cluster.py'
--- hooks/charmhelpers/contrib/hahelpers/cluster.py 2014-02-24 17:52:34 +0000
+++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2014-07-11 16:43:39 +0000
@@ -170,6 +170,7 @@
170170
171 :configs : OSTemplateRenderer: A config tempating object to inspect for171 :configs : OSTemplateRenderer: A config tempating object to inspect for
172 a complete https context.172 a complete https context.
173
173 :vip_setting: str: Setting in charm config that specifies174 :vip_setting: str: Setting in charm config that specifies
174 VIP address.175 VIP address.
175 '''176 '''
176177
=== added directory 'hooks/charmhelpers/contrib/openstack/amulet'
=== added file 'hooks/charmhelpers/contrib/openstack/amulet/__init__.py'
=== added file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2014-07-11 16:43:39 +0000
@@ -0,0 +1,57 @@
1from charmhelpers.contrib.amulet.deployment import (
2 AmuletDeployment
3)
4
5
6class OpenStackAmuletDeployment(AmuletDeployment):
7 """This class inherits from AmuletDeployment and has additional support
8 that is specifically for use by OpenStack charms."""
9
10 def __init__(self, series, openstack=None, source=None):
11 """Initialize the deployment environment."""
12 super(OpenStackAmuletDeployment, self).__init__(series)
13 self.openstack = openstack
14 self.source = source
15
16 def _add_services(self, this_service, other_services):
17 """Add services to the deployment and set openstack-origin."""
18 super(OpenStackAmuletDeployment, self)._add_services(this_service,
19 other_services)
20 name = 0
21 services = other_services
22 services.append(this_service)
23 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph']
24
25 if self.openstack:
26 for svc in services:
27 charm_name = self._get_charm_name(svc[name])
28 if charm_name not in use_source:
29 config = {'openstack-origin': self.openstack}
30 self.d.configure(svc[name], config)
31
32 if self.source:
33 for svc in services:
34 charm_name = self._get_charm_name(svc[name])
35 if charm_name in use_source:
36 config = {'source': self.source}
37 self.d.configure(svc[name], config)
38
39 def _configure_services(self, configs):
40 """Configure all of the services."""
41 for service, config in configs.iteritems():
42 self.d.configure(service, config)
43
44 def _get_openstack_release(self):
45 """Return an integer representing the enum value of the openstack
46 release."""
47 self.precise_essex, self.precise_folsom, self.precise_grizzly, \
48 self.precise_havana, self.precise_icehouse, \
49 self.trusty_icehouse = range(6)
50 releases = {
51 ('precise', None): self.precise_essex,
52 ('precise', 'cloud:precise-folsom'): self.precise_folsom,
53 ('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
54 ('precise', 'cloud:precise-havana'): self.precise_havana,
55 ('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
56 ('trusty', None): self.trusty_icehouse}
57 return releases[(self.series, self.openstack)]
058
=== added file 'hooks/charmhelpers/contrib/openstack/amulet/utils.py'
--- hooks/charmhelpers/contrib/openstack/amulet/utils.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2014-07-11 16:43:39 +0000
@@ -0,0 +1,253 @@
1import logging
2import os
3import time
4import urllib
5
6import glanceclient.v1.client as glance_client
7import keystoneclient.v2_0 as keystone_client
8import novaclient.v1_1.client as nova_client
9
10from charmhelpers.contrib.amulet.utils import (
11 AmuletUtils
12)
13
14DEBUG = logging.DEBUG
15ERROR = logging.ERROR
16
17
18class OpenStackAmuletUtils(AmuletUtils):
19 """This class inherits from AmuletUtils and has additional support
20 that is specifically for use by OpenStack charms."""
21
22 def __init__(self, log_level=ERROR):
23 """Initialize the deployment environment."""
24 super(OpenStackAmuletUtils, self).__init__(log_level)
25
26 def validate_endpoint_data(self, endpoints, admin_port, internal_port,
27 public_port, expected):
28 """Validate actual endpoint data vs expected endpoint data. The ports
29 are used to find the matching endpoint."""
30 found = False
31 for ep in endpoints:
32 self.log.debug('endpoint: {}'.format(repr(ep)))
33 if admin_port in ep.adminurl and internal_port in ep.internalurl \
34 and public_port in ep.publicurl:
35 found = True
36 actual = {'id': ep.id,
37 'region': ep.region,
38 'adminurl': ep.adminurl,
39 'internalurl': ep.internalurl,
40 'publicurl': ep.publicurl,
41 'service_id': ep.service_id}
42 ret = self._validate_dict_data(expected, actual)
43 if ret:
44 return 'unexpected endpoint data - {}'.format(ret)
45
46 if not found:
47 return 'endpoint not found'
48
49 def validate_svc_catalog_endpoint_data(self, expected, actual):
50 """Validate a list of actual service catalog endpoints vs a list of
51 expected service catalog endpoints."""
52 self.log.debug('actual: {}'.format(repr(actual)))
53 for k, v in expected.iteritems():
54 if k in actual:
55 ret = self._validate_dict_data(expected[k][0], actual[k][0])
56 if ret:
57 return self.endpoint_error(k, ret)
58 else:
59 return "endpoint {} does not exist".format(k)
60 return ret
61
62 def validate_tenant_data(self, expected, actual):
63 """Validate a list of actual tenant data vs list of expected tenant
64 data."""
65 self.log.debug('actual: {}'.format(repr(actual)))
66 for e in expected:
67 found = False
68 for act in actual:
69 a = {'enabled': act.enabled, 'description': act.description,
70 'name': act.name, 'id': act.id}
71 if e['name'] == a['name']:
72 found = True
73 ret = self._validate_dict_data(e, a)
74 if ret:
75 return "unexpected tenant data - {}".format(ret)
76 if not found:
77 return "tenant {} does not exist".format(e['name'])
78 return ret
79
80 def validate_role_data(self, expected, actual):
81 """Validate a list of actual role data vs a list of expected role
82 data."""
83 self.log.debug('actual: {}'.format(repr(actual)))
84 for e in expected:
85 found = False
86 for act in actual:
87 a = {'name': act.name, 'id': act.id}
88 if e['name'] == a['name']:
89 found = True
90 ret = self._validate_dict_data(e, a)
91 if ret:
92 return "unexpected role data - {}".format(ret)
93 if not found:
94 return "role {} does not exist".format(e['name'])
95 return ret
96
97 def validate_user_data(self, expected, actual):
98 """Validate a list of actual user data vs a list of expected user
99 data."""
100 self.log.debug('actual: {}'.format(repr(actual)))
101 for e in expected:
102 found = False
103 for act in actual:
104 a = {'enabled': act.enabled, 'name': act.name,
105 'email': act.email, 'tenantId': act.tenantId,
106 'id': act.id}
107 if e['name'] == a['name']:
108 found = True
109 ret = self._validate_dict_data(e, a)
110 if ret:
111 return "unexpected user data - {}".format(ret)
112 if not found:
113 return "user {} does not exist".format(e['name'])
114 return ret
115
116 def validate_flavor_data(self, expected, actual):
117 """Validate a list of actual flavors vs a list of expected flavors."""
118 self.log.debug('actual: {}'.format(repr(actual)))
119 act = [a.name for a in actual]
120 return self._validate_list_data(expected, act)
121
122 def tenant_exists(self, keystone, tenant):
123 """Return True if tenant exists"""
124 return tenant in [t.name for t in keystone.tenants.list()]
125
126 def authenticate_keystone_admin(self, keystone_sentry, user, password,
127 tenant):
128 """Authenticates admin user with the keystone admin endpoint."""
129 service_ip = \
130 keystone_sentry.relation('shared-db',
131 'mysql:shared-db')['private-address']
132 ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
133 return keystone_client.Client(username=user, password=password,
134 tenant_name=tenant, auth_url=ep)
135
136 def authenticate_keystone_user(self, keystone, user, password, tenant):
137 """Authenticates a regular user with the keystone public endpoint."""
138 ep = keystone.service_catalog.url_for(service_type='identity',
139 endpoint_type='publicURL')
140 return keystone_client.Client(username=user, password=password,
141 tenant_name=tenant, auth_url=ep)
142
143 def authenticate_glance_admin(self, keystone):
144 """Authenticates admin user with glance."""
145 ep = keystone.service_catalog.url_for(service_type='image',
146 endpoint_type='adminURL')
147 return glance_client.Client(ep, token=keystone.auth_token)
148
149 def authenticate_nova_user(self, keystone, user, password, tenant):
150 """Authenticates a regular user with nova-api."""
151 ep = keystone.service_catalog.url_for(service_type='identity',
152 endpoint_type='publicURL')
153 return nova_client.Client(username=user, api_key=password,
154 project_id=tenant, auth_url=ep)
155
156 def create_cirros_image(self, glance, image_name):
157 """Download the latest cirros image and upload it to glance."""
158 http_proxy = os.getenv('AMULET_HTTP_PROXY')
159 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
160 if http_proxy:
161 proxies = {'http': http_proxy}
162 opener = urllib.FancyURLopener(proxies)
163 else:
164 opener = urllib.FancyURLopener()
165
166 f = opener.open("http://download.cirros-cloud.net/version/released")
167 version = f.read().strip()
168 cirros_img = "tests/cirros-{}-x86_64-disk.img".format(version)
169
170 if not os.path.exists(cirros_img):
171 cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
172 version, cirros_img)
173 opener.retrieve(cirros_url, cirros_img)
174 f.close()
175
176 with open(cirros_img) as f:
177 image = glance.images.create(name=image_name, is_public=True,
178 disk_format='qcow2',
179 container_format='bare', data=f)
180 count = 1
181 status = image.status
182 while status != 'active' and count < 10:
183 time.sleep(3)
184 image = glance.images.get(image.id)
185 status = image.status
186 self.log.debug('image status: {}'.format(status))
187 count += 1
188
189 if status != 'active':
190 self.log.error('image creation timed out')
191 return None
192
193 return image
194
195 def delete_image(self, glance, image):
196 """Delete the specified image."""
197 num_before = len(list(glance.images.list()))
198 glance.images.delete(image)
199
200 count = 1
201 num_after = len(list(glance.images.list()))
202 while num_after != (num_before - 1) and count < 10:
203 time.sleep(3)
204 num_after = len(list(glance.images.list()))
205 self.log.debug('number of images: {}'.format(num_after))
206 count += 1
207
208 if num_after != (num_before - 1):
209 self.log.error('image deletion timed out')
210 return False
211
212 return True
213
214 def create_instance(self, nova, image_name, instance_name, flavor):
215 """Create the specified instance."""
216 image = nova.images.find(name=image_name)
217 flavor = nova.flavors.find(name=flavor)
218 instance = nova.servers.create(name=instance_name, image=image,
219 flavor=flavor)
220
221 count = 1
222 status = instance.status
223 while status != 'ACTIVE' and count < 60:
224 time.sleep(3)
225 instance = nova.servers.get(instance.id)
226 status = instance.status
227 self.log.debug('instance status: {}'.format(status))
228 count += 1
229
230 if status != 'ACTIVE':
231 self.log.error('instance creation timed out')
232 return None
233
234 return instance
235
236 def delete_instance(self, nova, instance):
237 """Delete the specified instance."""
238 num_before = len(list(nova.servers.list()))
239 nova.servers.delete(instance)
240
241 count = 1
242 num_after = len(list(nova.servers.list()))
243 while num_after != (num_before - 1) and count < 10:
244 time.sleep(3)
245 num_after = len(list(nova.servers.list()))
246 self.log.debug('number of instances: {}'.format(num_after))
247 count += 1
248
249 if num_after != (num_before - 1):
250 self.log.error('instance deletion timed out')
251 return False
252
253 return True
0254
=== modified file 'hooks/charmhelpers/contrib/openstack/context.py'
--- hooks/charmhelpers/contrib/openstack/context.py 2014-05-19 11:41:35 +0000
+++ hooks/charmhelpers/contrib/openstack/context.py 2014-07-11 16:43:39 +0000
@@ -243,23 +243,31 @@
243243
244244
245class AMQPContext(OSContextGenerator):245class AMQPContext(OSContextGenerator):
246 interfaces = ['amqp']
247246
248 def __init__(self, ssl_dir=None):247 def __init__(self, ssl_dir=None, rel_name='amqp', relation_prefix=None):
249 self.ssl_dir = ssl_dir248 self.ssl_dir = ssl_dir
249 self.rel_name = rel_name
250 self.relation_prefix = relation_prefix
251 self.interfaces = [rel_name]
250252
251 def __call__(self):253 def __call__(self):
252 log('Generating template context for amqp')254 log('Generating template context for amqp')
253 conf = config()255 conf = config()
256 user_setting = 'rabbit-user'
257 vhost_setting = 'rabbit-vhost'
258 if self.relation_prefix:
259 user_setting = self.relation_prefix + '-rabbit-user'
260 vhost_setting = self.relation_prefix + '-rabbit-vhost'
261
254 try:262 try:
255 username = conf['rabbit-user']263 username = conf[user_setting]
256 vhost = conf['rabbit-vhost']264 vhost = conf[vhost_setting]
257 except KeyError as e:265 except KeyError as e:
258 log('Could not generate shared_db context. '266 log('Could not generate shared_db context. '
259 'Missing required charm config options: %s.' % e)267 'Missing required charm config options: %s.' % e)
260 raise OSContextError268 raise OSContextError
261 ctxt = {}269 ctxt = {}
262 for rid in relation_ids('amqp'):270 for rid in relation_ids(self.rel_name):
263 ha_vip_only = False271 ha_vip_only = False
264 for unit in related_units(rid):272 for unit in related_units(rid):
265 if relation_get('clustered', rid=rid, unit=unit):273 if relation_get('clustered', rid=rid, unit=unit):
@@ -418,12 +426,13 @@
418 """426 """
419 Generates a context for an apache vhost configuration that configures427 Generates a context for an apache vhost configuration that configures
420 HTTPS reverse proxying for one or many endpoints. Generated context428 HTTPS reverse proxying for one or many endpoints. Generated context
421 looks something like:429 looks something like::
422 {430
423 'namespace': 'cinder',431 {
424 'private_address': 'iscsi.mycinderhost.com',432 'namespace': 'cinder',
425 'endpoints': [(8776, 8766), (8777, 8767)]433 'private_address': 'iscsi.mycinderhost.com',
426 }434 'endpoints': [(8776, 8766), (8777, 8767)]
435 }
427436
428 The endpoints list consists of a tuples mapping external ports437 The endpoints list consists of a tuples mapping external ports
429 to internal ports.438 to internal ports.
@@ -541,6 +550,26 @@
541550
542 return nvp_ctxt551 return nvp_ctxt
543552
553 def n1kv_ctxt(self):
554 driver = neutron_plugin_attribute(self.plugin, 'driver',
555 self.network_manager)
556 n1kv_config = neutron_plugin_attribute(self.plugin, 'config',
557 self.network_manager)
558 n1kv_ctxt = {
559 'core_plugin': driver,
560 'neutron_plugin': 'n1kv',
561 'neutron_security_groups': self.neutron_security_groups,
562 'local_ip': unit_private_ip(),
563 'config': n1kv_config,
564 'vsm_ip': config('n1kv-vsm-ip'),
565 'vsm_username': config('n1kv-vsm-username'),
566 'vsm_password': config('n1kv-vsm-password'),
567 'restrict_policy_profiles': config(
568 'n1kv_restrict_policy_profiles'),
569 }
570
571 return n1kv_ctxt
572
544 def neutron_ctxt(self):573 def neutron_ctxt(self):
545 if https():574 if https():
546 proto = 'https'575 proto = 'https'
@@ -572,6 +601,8 @@
572 ctxt.update(self.ovs_ctxt())601 ctxt.update(self.ovs_ctxt())
573 elif self.plugin in ['nvp', 'nsx']:602 elif self.plugin in ['nvp', 'nsx']:
574 ctxt.update(self.nvp_ctxt())603 ctxt.update(self.nvp_ctxt())
604 elif self.plugin == 'n1kv':
605 ctxt.update(self.n1kv_ctxt())
575606
576 alchemy_flags = config('neutron-alchemy-flags')607 alchemy_flags = config('neutron-alchemy-flags')
577 if alchemy_flags:608 if alchemy_flags:
@@ -611,7 +642,7 @@
611 The subordinate interface allows subordinates to export their642 The subordinate interface allows subordinates to export their
612 configuration requirements to the principle for multiple config643 configuration requirements to the principle for multiple config
613 files and multiple serivces. Ie, a subordinate that has interfaces644 files and multiple serivces. Ie, a subordinate that has interfaces
614 to both glance and nova may export to following yaml blob as json:645 to both glance and nova may export to following yaml blob as json::
615646
616 glance:647 glance:
617 /etc/glance/glance-api.conf:648 /etc/glance/glance-api.conf:
@@ -630,7 +661,8 @@
630661
631 It is then up to the principle charms to subscribe this context to662 It is then up to the principle charms to subscribe this context to
632 the service+config file it is interestd in. Configuration data will663 the service+config file it is interestd in. Configuration data will
633 be available in the template context, in glance's case, as:664 be available in the template context, in glance's case, as::
665
634 ctxt = {666 ctxt = {
635 ... other context ...667 ... other context ...
636 'subordinate_config': {668 'subordinate_config': {
637669
=== modified file 'hooks/charmhelpers/contrib/openstack/neutron.py'
--- hooks/charmhelpers/contrib/openstack/neutron.py 2014-05-19 11:41:35 +0000
+++ hooks/charmhelpers/contrib/openstack/neutron.py 2014-07-11 16:43:39 +0000
@@ -128,6 +128,20 @@
128 'server_packages': ['neutron-server',128 'server_packages': ['neutron-server',
129 'neutron-plugin-vmware'],129 'neutron-plugin-vmware'],
130 'server_services': ['neutron-server']130 'server_services': ['neutron-server']
131 },
132 'n1kv': {
133 'config': '/etc/neutron/plugins/cisco/cisco_plugins.ini',
134 'driver': 'neutron.plugins.cisco.network_plugin.PluginV2',
135 'contexts': [
136 context.SharedDBContext(user=config('neutron-database-user'),
137 database=config('neutron-database'),
138 relation_prefix='neutron',
139 ssl_dir=NEUTRON_CONF_DIR)],
140 'services': [],
141 'packages': [['neutron-plugin-cisco']],
142 'server_packages': ['neutron-server',
143 'neutron-plugin-cisco'],
144 'server_services': ['neutron-server']
131 }145 }
132 }146 }
133 if release >= 'icehouse':147 if release >= 'icehouse':
134148
=== modified file 'hooks/charmhelpers/contrib/openstack/templating.py'
--- hooks/charmhelpers/contrib/openstack/templating.py 2013-09-23 19:01:06 +0000
+++ hooks/charmhelpers/contrib/openstack/templating.py 2014-07-11 16:43:39 +0000
@@ -30,17 +30,17 @@
30 loading dir.30 loading dir.
3131
32 A charm may also ship a templates dir with this module32 A charm may also ship a templates dir with this module
33 and it will be appended to the bottom of the search list, eg:33 and it will be appended to the bottom of the search list, eg::
34 hooks/charmhelpers/contrib/openstack/templates.34
3535 hooks/charmhelpers/contrib/openstack/templates
36 :param templates_dir: str: Base template directory containing release36
37 sub-directories.37 :param templates_dir (str): Base template directory containing release
38 :param os_release : str: OpenStack release codename to construct template38 sub-directories.
39 loader.39 :param os_release (str): OpenStack release codename to construct template
4040 loader.
41 :returns : jinja2.ChoiceLoader constructed with a list of41 :returns: jinja2.ChoiceLoader constructed with a list of
42 jinja2.FilesystemLoaders, ordered in descending42 jinja2.FilesystemLoaders, ordered in descending
43 order by OpenStack release.43 order by OpenStack release.
44 """44 """
45 tmpl_dirs = [(rel, os.path.join(templates_dir, rel))45 tmpl_dirs = [(rel, os.path.join(templates_dir, rel))
46 for rel in OPENSTACK_CODENAMES.itervalues()]46 for rel in OPENSTACK_CODENAMES.itervalues()]
@@ -111,7 +111,8 @@
111 and ease the burden of managing config templates across multiple OpenStack111 and ease the burden of managing config templates across multiple OpenStack
112 releases.112 releases.
113113
114 Basic usage:114 Basic usage::
115
115 # import some common context generates from charmhelpers116 # import some common context generates from charmhelpers
116 from charmhelpers.contrib.openstack import context117 from charmhelpers.contrib.openstack import context
117118
@@ -131,21 +132,19 @@
131 # write out all registered configs132 # write out all registered configs
132 configs.write_all()133 configs.write_all()
133134
134 Details:135 **OpenStack Releases and template loading**
135136
136 OpenStack Releases and template loading
137 ---------------------------------------
138 When the object is instantiated, it is associated with a specific OS137 When the object is instantiated, it is associated with a specific OS
139 release. This dictates how the template loader will be constructed.138 release. This dictates how the template loader will be constructed.
140139
141 The constructed loader attempts to load the template from several places140 The constructed loader attempts to load the template from several places
142 in the following order:141 in the following order:
143 - from the most recent OS release-specific template dir (if one exists)142 - from the most recent OS release-specific template dir (if one exists)
144 - the base templates_dir143 - the base templates_dir
145 - a template directory shipped in the charm with this helper file.144 - a template directory shipped in the charm with this helper file.
146145
147146 For the example above, '/tmp/templates' contains the following structure::
148 For the example above, '/tmp/templates' contains the following structure:147
149 /tmp/templates/nova.conf148 /tmp/templates/nova.conf
150 /tmp/templates/api-paste.ini149 /tmp/templates/api-paste.ini
151 /tmp/templates/grizzly/api-paste.ini150 /tmp/templates/grizzly/api-paste.ini
@@ -169,8 +168,8 @@
169 $CHARM/hooks/charmhelpers/contrib/openstack/templates. This allows168 $CHARM/hooks/charmhelpers/contrib/openstack/templates. This allows
170 us to ship common templates (haproxy, apache) with the helpers.169 us to ship common templates (haproxy, apache) with the helpers.
171170
172 Context generators171 **Context generators**
173 ---------------------------------------172
174 Context generators are used to generate template contexts during hook173 Context generators are used to generate template contexts during hook
175 execution. Doing so may require inspecting service relations, charm174 execution. Doing so may require inspecting service relations, charm
176 config, etc. When registered, a config file is associated with a list175 config, etc. When registered, a config file is associated with a list
177176
=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
--- hooks/charmhelpers/contrib/openstack/utils.py 2014-06-19 10:11:47 +0000
+++ hooks/charmhelpers/contrib/openstack/utils.py 2014-07-11 16:43:39 +0000
@@ -84,6 +84,8 @@
84 '''Derive OpenStack release codename from a given installation source.'''84 '''Derive OpenStack release codename from a given installation source.'''
85 ubuntu_rel = lsb_release()['DISTRIB_CODENAME']85 ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
86 rel = ''86 rel = ''
87 if src is None:
88 return rel
87 if src in ['distro', 'distro-proposed']:89 if src in ['distro', 'distro-proposed']:
88 try:90 try:
89 rel = UBUNTU_OPENSTACK_RELEASE[ubuntu_rel]91 rel = UBUNTU_OPENSTACK_RELEASE[ubuntu_rel]
@@ -189,7 +191,7 @@
189 for version, cname in vers_map.iteritems():191 for version, cname in vers_map.iteritems():
190 if cname == codename:192 if cname == codename:
191 return version193 return version
192 #e = "Could not determine OpenStack version for package: %s" % pkg194 # e = "Could not determine OpenStack version for package: %s" % pkg
193 # error_out(e)195 # error_out(e)
194196
195197
196198
=== modified file 'hooks/charmhelpers/contrib/storage/linux/ceph.py'
--- hooks/charmhelpers/contrib/storage/linux/ceph.py 2014-02-24 17:52:34 +0000
+++ hooks/charmhelpers/contrib/storage/linux/ceph.py 2014-07-11 16:43:39 +0000
@@ -303,7 +303,7 @@
303 blk_device, fstype, system_services=[]):303 blk_device, fstype, system_services=[]):
304 """304 """
305 NOTE: This function must only be called from a single service unit for305 NOTE: This function must only be called from a single service unit for
306 the same rbd_img otherwise data loss will occur.306 the same rbd_img otherwise data loss will occur.
307307
308 Ensures given pool and RBD image exists, is mapped to a block device,308 Ensures given pool and RBD image exists, is mapped to a block device,
309 and the device is formatted and mounted at the given mount_point.309 and the device is formatted and mounted at the given mount_point.
310310
=== modified file 'hooks/charmhelpers/contrib/storage/linux/utils.py'
--- hooks/charmhelpers/contrib/storage/linux/utils.py 2014-05-19 11:41:35 +0000
+++ hooks/charmhelpers/contrib/storage/linux/utils.py 2014-07-11 16:43:39 +0000
@@ -37,6 +37,7 @@
37 check_call(['dd', 'if=/dev/zero', 'of=%s' % (block_device),37 check_call(['dd', 'if=/dev/zero', 'of=%s' % (block_device),
38 'bs=512', 'count=100', 'seek=%s' % (gpt_end)])38 'bs=512', 'count=100', 'seek=%s' % (gpt_end)])
3939
40
40def is_device_mounted(device):41def is_device_mounted(device):
41 '''Given a device path, return True if that device is mounted, and False42 '''Given a device path, return True if that device is mounted, and False
42 if it isn't.43 if it isn't.
4344
=== modified file 'hooks/charmhelpers/core/hookenv.py'
--- hooks/charmhelpers/core/hookenv.py 2014-05-19 11:41:35 +0000
+++ hooks/charmhelpers/core/hookenv.py 2014-07-11 16:43:39 +0000
@@ -25,7 +25,7 @@
25def cached(func):25def cached(func):
26 """Cache return values for multiple executions of func + args26 """Cache return values for multiple executions of func + args
2727
28 For example:28 For example::
2929
30 @cached30 @cached
31 def unit_get(attribute):31 def unit_get(attribute):
@@ -445,18 +445,19 @@
445class Hooks(object):445class Hooks(object):
446 """A convenient handler for hook functions.446 """A convenient handler for hook functions.
447447
448 Example:448 Example::
449
449 hooks = Hooks()450 hooks = Hooks()
450451
451 # register a hook, taking its name from the function name452 # register a hook, taking its name from the function name
452 @hooks.hook()453 @hooks.hook()
453 def install():454 def install():
454 ...455 pass # your code here
455456
456 # register a hook, providing a custom hook name457 # register a hook, providing a custom hook name
457 @hooks.hook("config-changed")458 @hooks.hook("config-changed")
458 def config_changed():459 def config_changed():
459 ...460 pass # your code here
460461
461 if __name__ == "__main__":462 if __name__ == "__main__":
462 # execute a hook based on the name the program is called by463 # execute a hook based on the name the program is called by
463464
=== modified file 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py 2014-06-19 10:11:47 +0000
+++ hooks/charmhelpers/core/host.py 2014-07-11 16:43:39 +0000
@@ -211,13 +211,13 @@
211def restart_on_change(restart_map, stopstart=False):211def restart_on_change(restart_map, stopstart=False):
212 """Restart services based on configuration files changing212 """Restart services based on configuration files changing
213213
214 This function is used a decorator, for example214 This function is used a decorator, for example::
215215
216 @restart_on_change({216 @restart_on_change({
217 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]217 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
218 })218 })
219 def ceph_client_changed():219 def ceph_client_changed():
220 ...220 pass # your code here
221221
222 In this example, the cinder-api and cinder-volume services222 In this example, the cinder-api and cinder-volume services
223 would be restarted if /etc/ceph/ceph.conf is changed by the223 would be restarted if /etc/ceph/ceph.conf is changed by the
@@ -313,9 +313,11 @@
313313
314def cmp_pkgrevno(package, revno, pkgcache=None):314def cmp_pkgrevno(package, revno, pkgcache=None):
315 '''Compare supplied revno with the revno of the installed package315 '''Compare supplied revno with the revno of the installed package
316 1 => Installed revno is greater than supplied arg316
317 0 => Installed revno is the same as supplied arg317 * 1 => Installed revno is greater than supplied arg
318 -1 => Installed revno is less than supplied arg318 * 0 => Installed revno is the same as supplied arg
319 * -1 => Installed revno is less than supplied arg
320
319 '''321 '''
320 import apt_pkg322 import apt_pkg
321 if not pkgcache:323 if not pkgcache:
322324
=== modified file 'hooks/charmhelpers/fetch/__init__.py'
--- hooks/charmhelpers/fetch/__init__.py 2014-06-19 10:11:47 +0000
+++ hooks/charmhelpers/fetch/__init__.py 2014-07-11 16:43:39 +0000
@@ -235,31 +235,39 @@
235 sources_var='install_sources',235 sources_var='install_sources',
236 keys_var='install_keys'):236 keys_var='install_keys'):
237 """237 """
238 Configure multiple sources from charm configuration238 Configure multiple sources from charm configuration.
239
240 The lists are encoded as yaml fragments in the configuration.
241 The frament needs to be included as a string.
239242
240 Example config:243 Example config:
241 install_sources:244 install_sources: |
242 - "ppa:foo"245 - "ppa:foo"
243 - "http://example.com/repo precise main"246 - "http://example.com/repo precise main"
244 install_keys:247 install_keys: |
245 - null248 - null
246 - "a1b2c3d4"249 - "a1b2c3d4"
247250
248 Note that 'null' (a.k.a. None) should not be quoted.251 Note that 'null' (a.k.a. None) should not be quoted.
249 """252 """
250 sources = safe_load(config(sources_var))253 sources = safe_load((config(sources_var) or '').strip()) or []
251 keys = config(keys_var)254 keys = safe_load((config(keys_var) or '').strip()) or None
252 if keys is not None:255
253 keys = safe_load(keys)256 if isinstance(sources, basestring):
254 if isinstance(sources, basestring) and (257 sources = [sources]
255 keys is None or isinstance(keys, basestring)):258
256 add_source(sources, keys)259 if keys is None:
260 for source in sources:
261 add_source(source, None)
257 else:262 else:
258 if not len(sources) == len(keys):263 if isinstance(keys, basestring):
259 msg = 'Install sources and keys lists are different lengths'264 keys = [keys]
260 raise SourceConfigError(msg)265
261 for src_num in range(len(sources)):266 if len(sources) != len(keys):
262 add_source(sources[src_num], keys[src_num])267 raise SourceConfigError(
268 'Install sources and keys lists are different lengths')
269 for source, key in zip(sources, keys):
270 add_source(source, key)
263 if update:271 if update:
264 apt_update(fatal=True)272 apt_update(fatal=True)
265273
266274
=== added directory 'tests'
=== added file 'tests/00-setup'
--- tests/00-setup 1970-01-01 00:00:00 +0000
+++ tests/00-setup 2014-07-11 16:43:39 +0000
@@ -0,0 +1,11 @@
1#!/bin/bash
2
3set -ex
4
5sudo add-apt-repository --yes ppa:juju/stable
6sudo apt-get update --yes
7sudo apt-get install --yes python-amulet
8sudo apt-get install --yes python-swiftclient
9sudo apt-get install --yes python-glanceclient
10sudo apt-get install --yes python-keystoneclient
11sudo apt-get install --yes python-novaclient
012
=== added file 'tests/10-basic-precise-essex'
--- tests/10-basic-precise-essex 1970-01-01 00:00:00 +0000
+++ tests/10-basic-precise-essex 2014-07-11 16:43:39 +0000
@@ -0,0 +1,9 @@
1#!/usr/bin/python
2
3"""Amulet tests on a basic swift-storage deployment on precise-essex."""
4
5from basic_deployment import SwiftStorageBasicDeployment
6
7if __name__ == '__main__':
8 deployment = SwiftStorageBasicDeployment(series='precise')
9 deployment.run_tests()
010
=== added file 'tests/11-basic-precise-folsom'
--- tests/11-basic-precise-folsom 1970-01-01 00:00:00 +0000
+++ tests/11-basic-precise-folsom 2014-07-11 16:43:39 +0000
@@ -0,0 +1,11 @@
1#!/usr/bin/python
2
3"""Amulet tests on a basic swift-storage deployment on precise-folsom."""
4
5from basic_deployment import SwiftStorageBasicDeployment
6
7if __name__ == '__main__':
8 deployment = SwiftStorageBasicDeployment(series='precise',
9 openstack='cloud:precise-folsom',
10 source='cloud:precise-updates/folsom')
11 deployment.run_tests()
012
=== added file 'tests/12-basic-precise-grizzly'
--- tests/12-basic-precise-grizzly 1970-01-01 00:00:00 +0000
+++ tests/12-basic-precise-grizzly 2014-07-11 16:43:39 +0000
@@ -0,0 +1,11 @@
1#!/usr/bin/python
2
3"""Amulet tests on a basic swift-storage deployment on precise-grizzly."""
4
5from basic_deployment import SwiftStorageBasicDeployment
6
7if __name__ == '__main__':
8 deployment = SwiftStorageBasicDeployment(series='precise',
9 openstack='cloud:precise-grizzly',
10 source='cloud:precise-updates/grizzly')
11 deployment.run_tests()
012
=== added file 'tests/13-basic-precise-havana'
--- tests/13-basic-precise-havana 1970-01-01 00:00:00 +0000
+++ tests/13-basic-precise-havana 2014-07-11 16:43:39 +0000
@@ -0,0 +1,11 @@
1#!/usr/bin/python
2
3"""Amulet tests on a basic swift-storage deployment on precise-havana."""
4
5from basic_deployment import SwiftStorageBasicDeployment
6
7if __name__ == '__main__':
8 deployment = SwiftStorageBasicDeployment(series='precise',
9 openstack='cloud:precise-havana',
10 source='cloud:precise-updates/havana')
11 deployment.run_tests()
012
=== added file 'tests/14-basic-precise-icehouse'
--- tests/14-basic-precise-icehouse 1970-01-01 00:00:00 +0000
+++ tests/14-basic-precise-icehouse 2014-07-11 16:43:39 +0000
@@ -0,0 +1,11 @@
1#!/usr/bin/python
2
3"""Amulet tests on a basic swift-storage deployment on precise-icehouse."""
4
5from basic_deployment import SwiftStorageBasicDeployment
6
7if __name__ == '__main__':
8 deployment = SwiftStorageBasicDeployment(series='precise',
9 openstack='cloud:precise-icehouse',
10 source='cloud:precise-updates/icehouse')
11 deployment.run_tests()
012
=== added file 'tests/15-basic-trusty-icehouse'
--- tests/15-basic-trusty-icehouse 1970-01-01 00:00:00 +0000
+++ tests/15-basic-trusty-icehouse 2014-07-11 16:43:39 +0000
@@ -0,0 +1,9 @@
1#!/usr/bin/python
2
3"""Amulet tests on a basic swift-storage deployment on trusty-icehouse."""
4
5from basic_deployment import SwiftStorageBasicDeployment
6
7if __name__ == '__main__':
8 deployment = SwiftStorageBasicDeployment(series='trusty')
9 deployment.run_tests()
010
=== added file 'tests/README'
--- tests/README 1970-01-01 00:00:00 +0000
+++ tests/README 2014-07-11 16:43:39 +0000
@@ -0,0 +1,52 @@
1This directory provides Amulet tests that focus on verification of swift-storage
2deployments.
3
4If you use a web proxy server to access the web, you'll need to set the
5AMULET_HTTP_PROXY environment variable to the http URL of the proxy server.
6
7The following examples demonstrate different ways that tests can be executed.
8All examples are run from the charm's root directory.
9
10 * To run all tests (starting with 00-setup):
11
12 make test
13
14 * To run a specific test module (or modules):
15
16 juju test -v -p AMULET_HTTP_PROXY 15-basic-trusty-icehouse
17
18 * To run a specific test module (or modules), and keep the environment
19 deployed after a failure:
20
21 juju test --set-e -v -p AMULET_HTTP_PROXY 15-basic-trusty-icehouse
22
23 * To re-run a test module against an already deployed environment (one
24 that was deployed by a previous call to 'juju test --set-e'):
25
26 ./tests/15-basic-trusty-icehouse
27
28For debugging and test development purposes, all code should be idempotent.
29In other words, the code should have the ability to be re-run without changing
30the results beyond the initial run. This enables editing and re-running of a
31test module against an already deployed environment, as described above.
32
33Manual debugging tips:
34
35 * Set the following env vars before using the OpenStack CLI as admin:
36 export OS_AUTH_URL=http://`juju-deployer -f keystone 2>&1 | tail -n 1`:5000/v2.0
37 export OS_TENANT_NAME=admin
38 export OS_USERNAME=admin
39 export OS_PASSWORD=openstack
40 export OS_REGION_NAME=RegionOne
41
42 * Set the following env vars before using the OpenStack CLI as demoUser:
43 export OS_AUTH_URL=http://`juju-deployer -f keystone 2>&1 | tail -n 1`:5000/v2.0
44 export OS_TENANT_NAME=demoTenant
45 export OS_USERNAME=demoUser
46 export OS_PASSWORD=password
47 export OS_REGION_NAME=RegionOne
48
49 * Sample swift command:
50 swift -A $OS_AUTH_URL --os-tenant-name services --os-username swift \
51 --os-password password list
52 (where tenant/user names and password are in swift-proxy's nova.conf file)
053
=== added file 'tests/basic_deployment.py'
--- tests/basic_deployment.py 1970-01-01 00:00:00 +0000
+++ tests/basic_deployment.py 2014-07-11 16:43:39 +0000
@@ -0,0 +1,450 @@
1#!/usr/bin/python
2
3import amulet
4import swiftclient
5
6from charmhelpers.contrib.openstack.amulet.deployment import (
7 OpenStackAmuletDeployment
8)
9
10from charmhelpers.contrib.openstack.amulet.utils import (
11 OpenStackAmuletUtils,
12 DEBUG, # flake8: noqa
13 ERROR
14)
15
16# Use DEBUG to turn on debug logging
17u = OpenStackAmuletUtils(ERROR)
18
19
20class SwiftStorageBasicDeployment(OpenStackAmuletDeployment):
21 """Amulet tests on a basic swift-storage deployment."""
22
23 def __init__(self, series, openstack=None, source=None):
24 """Deploy the entire test environment."""
25 super(SwiftStorageBasicDeployment, self).__init__(series, openstack,
26 source)
27 self._add_services()
28 self._add_relations()
29 self._configure_services()
30 self._deploy()
31 self._initialize_tests()
32
33 def _add_services(self):
34 """Add the service that we're testing, including the number of units,
35 where swift-storage is local, and the other charms are from
36 the charm store."""
37 this_service = ('swift-storage', 1)
38 other_services = [('mysql', 1),
39 ('keystone', 1), ('glance', 1), ('swift-proxy', 1)]
40 super(SwiftStorageBasicDeployment, self)._add_services(this_service,
41 other_services)
42
43 def _add_relations(self):
44 """Add all of the relations for the services."""
45 relations = {
46 'keystone:shared-db': 'mysql:shared-db',
47 'swift-proxy:identity-service': 'keystone:identity-service',
48 'swift-storage:swift-storage': 'swift-proxy:swift-storage',
49 'glance:identity-service': 'keystone:identity-service',
50 'glance:shared-db': 'mysql:shared-db',
51 'glance:object-store': 'swift-proxy:object-store'
52 }
53 super(SwiftStorageBasicDeployment, self)._add_relations(relations)
54
55 def _configure_services(self):
56 """Configure all of the services."""
57 keystone_config = {'admin-password': 'openstack',
58 'admin-token': 'ubuntutesting'}
59 swift_proxy_config = {'zone-assignment': 'manual',
60 'replicas': '1',
61 'swift-hash': 'fdfef9d4-8b06-11e2-8ac0-531c923c8fae',
62 'use-https': 'no'}
63 swift_storage_config = {'zone': '1',
64 'block-device': 'vdb',
65 'overwrite': 'true'}
66 configs = {'keystone': keystone_config,
67 'swift-proxy': swift_proxy_config,
68 'swift-storage': swift_storage_config}
69 super(SwiftStorageBasicDeployment, self)._configure_services(configs)
70
71 def _initialize_tests(self):
72 """Perform final initialization before tests get run."""
73 # Access the sentries for inspecting service units
74 self.mysql_sentry = self.d.sentry.unit['mysql/0']
75 self.keystone_sentry = self.d.sentry.unit['keystone/0']
76 self.glance_sentry = self.d.sentry.unit['glance/0']
77 self.swift_proxy_sentry = self.d.sentry.unit['swift-proxy/0']
78 self.swift_storage_sentry = self.d.sentry.unit['swift-storage/0']
79
80 # Authenticate admin with keystone
81 self.keystone = u.authenticate_keystone_admin(self.keystone_sentry,
82 user='admin',
83 password='openstack',
84 tenant='admin')
85
86 # Authenticate admin with glance endpoint
87 self.glance = u.authenticate_glance_admin(self.keystone)
88
89 # Authenticate swift user
90 keystone_relation = self.keystone_sentry.relation('identity-service',
91 'swift-proxy:identity-service')
92 ep = self.keystone.service_catalog.url_for(service_type='identity',
93 endpoint_type='publicURL')
94 self.swift = swiftclient.Connection(authurl=ep,
95 user=keystone_relation['service_username'],
96 key=keystone_relation['service_password'],
97 tenant_name=keystone_relation['service_tenant'],
98 auth_version='2.0')
99
100 # Create a demo tenant/role/user
101 self.demo_tenant = 'demoTenant'
102 self.demo_role = 'demoRole'
103 self.demo_user = 'demoUser'
104 if not u.tenant_exists(self.keystone, self.demo_tenant):
105 tenant = self.keystone.tenants.create(tenant_name=self.demo_tenant,
106 description='demo tenant',
107 enabled=True)
108 self.keystone.roles.create(name=self.demo_role)
109 self.keystone.users.create(name=self.demo_user,
110 password='password',
111 tenant_id=tenant.id,
112 email='demo@demo.com')
113
114 # Authenticate demo user with keystone
115 self.keystone_demo = \
116 u.authenticate_keystone_user(self.keystone, user=self.demo_user,
117 password='password',
118 tenant=self.demo_tenant)
119
120 def test_services(self):
121 """Verify the expected services are running on the corresponding
122 service units."""
123 swift_storage_services = ['status swift-account',
124 'status swift-account-auditor',
125 'status swift-account-reaper',
126 'status swift-account-replicator',
127 'status swift-container',
128 'status swift-container-auditor',
129 'status swift-container-replicator',
130 'status swift-container-updater',
131 'status swift-object',
132 'status swift-object-auditor',
133 'status swift-object-replicator',
134 'status swift-object-updater']
135 if self._get_openstack_release() >= self.precise_icehouse:
136 swift_storage_services.append('status swift-container-sync')
137 commands = {
138 self.mysql_sentry: ['status mysql'],
139 self.keystone_sentry: ['status keystone'],
140 self.glance_sentry: ['status glance-registry', 'status glance-api'],
141 self.swift_proxy_sentry: ['status swift-proxy'],
142 self.swift_storage_sentry: swift_storage_services
143 }
144
145 ret = u.validate_services(commands)
146 if ret:
147 amulet.raise_status(amulet.FAIL, msg=ret)
148
149 def test_users(self):
150 """Verify all existing roles."""
151 user1 = {'name': 'demoUser',
152 'enabled': True,
153 'tenantId': u.not_null,
154 'id': u.not_null,
155 'email': 'demo@demo.com'}
156 user2 = {'name': 'admin',
157 'enabled': True,
158 'tenantId': u.not_null,
159 'id': u.not_null,
160 'email': 'juju@localhost'}
161 user3 = {'name': 'glance',
162 'enabled': True,
163 'tenantId': u.not_null,
164 'id': u.not_null,
165 'email': u'juju@localhost'}
166 user4 = {'name': 'swift',
167 'enabled': True,
168 'tenantId': u.not_null,
169 'id': u.not_null,
170 'email': u'juju@localhost'}
171 expected = [user1, user2, user3, user4]
172 actual = self.keystone.users.list()
173
174 ret = u.validate_user_data(expected, actual)
175 if ret:
176 amulet.raise_status(amulet.FAIL, msg=ret)
177
178 def test_service_catalog(self):
179 """Verify that the service catalog endpoint data is valid."""
180 endpoint_vol = {'adminURL': u.valid_url,
181 'region': 'RegionOne',
182 'publicURL': u.valid_url,
183 'internalURL': u.valid_url}
184 endpoint_id = {'adminURL': u.valid_url,
185 'region': 'RegionOne',
186 'publicURL': u.valid_url,
187 'internalURL': u.valid_url}
188 if self._get_openstack_release() >= self.precise_folsom:
189 endpoint_vol['id'] = u.not_null
190 endpoint_id['id'] = u.not_null
191 expected = {'image': [endpoint_id], 'object-store': [endpoint_id],
192 'identity': [endpoint_id]}
193 actual = self.keystone_demo.service_catalog.get_endpoints()
194
195 ret = u.validate_svc_catalog_endpoint_data(expected, actual)
196 if ret:
197 amulet.raise_status(amulet.FAIL, msg=ret)
198
199 def test_openstack_object_store_endpoint(self):
200 """Verify the swift object-store endpoint data."""
201 endpoints = self.keystone.endpoints.list()
202 admin_port = internal_port = public_port = '8080'
203 expected = {'id': u.not_null,
204 'region': 'RegionOne',
205 'adminurl': u.valid_url,
206 'internalurl': u.valid_url,
207 'publicurl': u.valid_url,
208 'service_id': u.not_null}
209
210 ret = u.validate_endpoint_data(endpoints, admin_port, internal_port,
211 public_port, expected)
212 if ret:
213 message = 'object-store endpoint: {}'.format(ret)
214 amulet.raise_status(amulet.FAIL, msg=message)
215
216 def test_swift_storage_swift_storage_relation(self):
217 """Verify the swift-storage to swift-proxy swift-storage relation
218 data."""
219 unit = self.swift_storage_sentry
220 relation = ['swift-storage', 'swift-proxy:swift-storage']
221 expected = {
222 'account_port': '6002',
223 'zone': '1',
224 'object_port': '6000',
225 'container_port': '6001',
226 'private-address': u.valid_ip,
227 'device': 'vdb'
228 }
229
230 ret = u.validate_relation_data(unit, relation, expected)
231 if ret:
232 message = u.relation_error('swift-storage swift-storage', ret)
233 amulet.raise_status(amulet.FAIL, msg=message)
234
235 def test_swift_proxy_swift_storage_relation(self):
236 """Verify the swift-proxy to swift-storage swift-storage relation
237 data."""
238 unit = self.swift_proxy_sentry
239 relation = ['swift-storage', 'swift-storage:swift-storage']
240 expected = {
241 'private-address': u.valid_ip,
242 'trigger': u.not_null,
243 'rings_url': u.valid_url,
244 'swift_hash': u.not_null
245 }
246
247 ret = u.validate_relation_data(unit, relation, expected)
248 if ret:
249 message = u.relation_error('swift-proxy swift-storage', ret)
250 amulet.raise_status(amulet.FAIL, msg=message)
251
252 def test_restart_on_config_change(self):
253 """Verify that the specified services are restarted when the config
254 is changed."""
255 # NOTE(coreycb): Skipping failing test on until resolved. This test
256 # fails because the config file's last mod time is
257 # slightly after the process' last mod time.
258 if self._get_openstack_release() >= self.precise_essex:
259 u.log.error("Skipping failing test until resolved")
260 return
261
262 services = {'swift-account-server': 'account-server.conf',
263 'swift-account-auditor': 'account-server.conf',
264 'swift-account-reaper': 'account-server.conf',
265 'swift-account-replicator': 'account-server.conf',
266 'swift-container-server': 'container-server.conf',
267 'swift-container-auditor': 'container-server.conf',
268 'swift-container-replicator': 'container-server.conf',
269 'swift-container-updater': 'container-server.conf',
270 'swift-object-server': 'object-server.conf',
271 'swift-object-auditor': 'object-server.conf',
272 'swift-object-replicator': 'object-server.conf',
273 'swift-object-updater': 'object-server.conf'}
274 if self._get_openstack_release() >= self.precise_icehouse:
275 services['swift-container-sync'] = 'container-server.conf'
276
277 self.d.configure('swift-storage',
278 {'object-server-threads-per-disk': '2'})
279
280 time = 20
281 for s, conf in services.iteritems():
282 config = '/etc/swift/{}'.format(conf)
283 if not u.service_restarted(self.swift_storage_sentry, s, config,
284 pgrep_full=True, sleep_time=time):
285 msg = "service {} didn't restart after config change".format(s)
286 amulet.raise_status(amulet.FAIL, msg=msg)
287 time = 0
288
289 self.d.configure('swift-storage',
290 {'object-server-threads-per-disk': '4'})
291
292 def test_swift_config(self):
293 """Verify the data in the swift-hash section of the swift config
294 file."""
295 unit = self.swift_storage_sentry
296 conf = '/etc/swift/swift.conf'
297 swift_proxy_relation = self.swift_proxy_sentry.relation('swift-storage',
298 'swift-storage:swift-storage')
299 expected = {
300 'swift_hash_path_suffix': swift_proxy_relation['swift_hash']
301 }
302
303 ret = u.validate_config_data(unit, conf, 'swift-hash', expected)
304 if ret:
305 message = "swift config error: {}".format(ret)
306 amulet.raise_status(amulet.FAIL, msg=message)
307
308 def test_account_server_config(self):
309 """Verify the data in the account server config file."""
310 unit = self.swift_storage_sentry
311 conf = '/etc/swift/account-server.conf'
312 expected = {
313 'DEFAULT': {
314 'bind_ip': '0.0.0.0',
315 'bind_port': '6002',
316 'workers': '1'
317 },
318 'pipeline:main': {
319 'pipeline': 'recon account-server'
320 },
321 'filter:recon': {
322 'use': 'egg:swift#recon',
323 'recon_cache_path': '/var/cache/swift'
324 },
325 'app:account-server': {
326 'use': 'egg:swift#account'
327 }
328 }
329
330 for section, pairs in expected.iteritems():
331 ret = u.validate_config_data(unit, conf, section, pairs)
332 if ret:
333 message = "account server config error: {}".format(ret)
334 amulet.raise_status(amulet.FAIL, msg=message)
335
336 def test_container_server_config(self):
337 """Verify the data in the container server config file."""
338 unit = self.swift_storage_sentry
339 conf = '/etc/swift/container-server.conf'
340 expected = {
341 'DEFAULT': {
342 'bind_ip': '0.0.0.0',
343 'bind_port': '6001',
344 'workers': '1'
345 },
346 'pipeline:main': {
347 'pipeline': 'recon container-server'
348 },
349 'filter:recon': {
350 'use': 'egg:swift#recon',
351 'recon_cache_path': '/var/cache/swift'
352 },
353 'app:container-server': {
354 'use': 'egg:swift#container',
355 'allow_versions': 'true'
356 }
357 }
358
359 for section, pairs in expected.iteritems():
360 ret = u.validate_config_data(unit, conf, section, pairs)
361 if ret:
362 message = "container server config error: {}".format(ret)
363 amulet.raise_status(amulet.FAIL, msg=message)
364
365 def test_object_server_config(self):
366 """Verify the data in the object server config file."""
367 unit = self.swift_storage_sentry
368 conf = '/etc/swift/object-server.conf'
369 expected = {
370 'DEFAULT': {
371 'bind_ip': '0.0.0.0',
372 'bind_port': '6000',
373 'workers': '1'
374 },
375 'pipeline:main': {
376 'pipeline': 'recon object-server'
377 },
378 'filter:recon': {
379 'use': 'egg:swift#recon',
380 'recon_cache_path': '/var/cache/swift'
381 },
382 'app:object-server': {
383 'use': 'egg:swift#object',
384 'threads_per_disk': '4'
385 }
386 }
387
388 for section, pairs in expected.iteritems():
389 ret = u.validate_config_data(unit, conf, section, pairs)
390 if ret:
391 message = "object server config error: {}".format(ret)
392 amulet.raise_status(amulet.FAIL, msg=message)
393
394 def test_image_create(self):
395 """Create an instance in glance, which is backed by swift, and validate
396 that some of the metadata for the image match in glance and swift."""
397 # NOTE(coreycb): Skipping failing test on folsom until resolved. On
398 # folsom only, uploading an image to glance gets 400 Bad
399 # Request - Error uploading image: (error): [Errno 111]
400 # ECONNREFUSED (HTTP 400)
401 if self._get_openstack_release() == self.precise_folsom:
402 u.log.error("Skipping failing test until resolved")
403 return
404
405 # Create glance image
406 image = u.create_cirros_image(self.glance, "cirros-image")
407 if not image:
408 amulet.raise_status(amulet.FAIL, msg="Image create failed")
409
410 # Validate that cirros image exists in glance and get its checksum/size
411 images = list(self.glance.images.list())
412 if len(images) != 1:
413 msg = "Expected 1 glance image, found {}".format(len(images))
414 amulet.raise_status(amulet.FAIL, msg=msg)
415
416 if images[0].name != 'cirros-image':
417 message = "cirros image does not exist"
418 amulet.raise_status(amulet.FAIL, msg=message)
419
420 glance_image_md5 = image.checksum
421 glance_image_size = image.size
422
423 # Validate that swift object's checksum/size match that from glance
424 headers, containers = self.swift.get_account()
425 if len(containers) != 1:
426 msg = "Expected 1 swift container, found {}".format(len(containers))
427 amulet.raise_status(amulet.FAIL, msg=msg)
428
429 container_name = containers[0].get('name')
430
431 headers, objects = self.swift.get_container(container_name)
432 if len(objects) != 1:
433 msg = "Expected 1 swift object, found {}".format(len(objects))
434 amulet.raise_status(amulet.FAIL, msg=msg)
435
436 swift_object_size = objects[0].get('bytes')
437 swift_object_md5 = objects[0].get('hash')
438
439 if glance_image_size != swift_object_size:
440 msg = "Glance image size {} != swift object size {}".format( \
441 glance_image_size, swift_object_size)
442 amulet.raise_status(amulet.FAIL, msg=msg)
443
444 if glance_image_md5 != swift_object_md5:
445 msg = "Glance image hash {} != swift object hash {}".format( \
446 glance_image_md5, swift_object_md5)
447 amulet.raise_status(amulet.FAIL, msg=msg)
448
449 # Cleanup
450 u.delete_image(self.glance, image)
0451
=== added directory 'tests/charmhelpers'
=== added file 'tests/charmhelpers/__init__.py'
=== added directory 'tests/charmhelpers/contrib'
=== added file 'tests/charmhelpers/contrib/__init__.py'
=== added directory 'tests/charmhelpers/contrib/amulet'
=== added file 'tests/charmhelpers/contrib/amulet/__init__.py'
=== added file 'tests/charmhelpers/contrib/amulet/deployment.py'
--- tests/charmhelpers/contrib/amulet/deployment.py 1970-01-01 00:00:00 +0000
+++ tests/charmhelpers/contrib/amulet/deployment.py 2014-07-11 16:43:39 +0000
@@ -0,0 +1,63 @@
1import amulet
2import re
3
4
5class AmuletDeployment(object):
6 """This class provides generic Amulet deployment and test runner
7 methods."""
8
9 def __init__(self, series):
10 """Initialize the deployment environment."""
11 self.series = series
12 self.d = amulet.Deployment(series=self.series)
13
14 def _get_charm_name(self, service_name):
15 """Gets the charm name from the service name. Unique service names can
16 be specified with a '-service#' suffix (e.g. mysql-service1)."""
17 if re.match(r"^.*-service\d{1,3}$", service_name):
18 charm_name = re.sub('\-service\d{1,3}$', '', service_name)
19 else:
20 charm_name = service_name
21 return charm_name
22
23 def _add_services(self, this_service, other_services):
24 """Add services to the deployment where this_service is the local charm
25 that we're focused on testing and other_services are the other
26 charms that come from the charm store."""
27 name, units = range(2)
28
29 charm_name = self._get_charm_name(this_service[name])
30 self.d.add(this_service[name],
31 units=this_service[units])
32
33 for svc in other_services:
34 charm_name = self._get_charm_name(svc[name])
35 self.d.add(svc[name],
36 charm='cs:{}/{}'.format(self.series, charm_name),
37 units=svc[units])
38
39 def _add_relations(self, relations):
40 """Add all of the relations for the services."""
41 for k, v in relations.iteritems():
42 self.d.relate(k, v)
43
44 def _configure_services(self, configs):
45 """Configure all of the services."""
46 for service, config in configs.iteritems():
47 self.d.configure(service, config)
48
49 def _deploy(self):
50 """Deploy environment and wait for all hooks to finish executing."""
51 try:
52 self.d.setup()
53 self.d.sentry.wait()
54 except amulet.helpers.TimeoutError:
55 amulet.raise_status(amulet.FAIL, msg="Deployment timed out")
56 except:
57 raise
58
59 def run_tests(self):
60 """Run all of the methods that are prefixed with 'test_'."""
61 for test in dir(self):
62 if test.startswith('test_'):
63 getattr(self, test)()
064
=== added file 'tests/charmhelpers/contrib/amulet/utils.py'
--- tests/charmhelpers/contrib/amulet/utils.py 1970-01-01 00:00:00 +0000
+++ tests/charmhelpers/contrib/amulet/utils.py 2014-07-11 16:43:39 +0000
@@ -0,0 +1,157 @@
1import ConfigParser
2import io
3import logging
4import re
5import sys
6from time import sleep
7
8
9class AmuletUtils(object):
10 """This class provides common utility functions that are used by Amulet
11 tests."""
12
13 def __init__(self, log_level=logging.ERROR):
14 self.log = self.get_logger(level=log_level)
15
16 def get_logger(self, name="amulet-logger", level=logging.DEBUG):
17 """Get a logger object that will log to stdout."""
18 log = logging
19 logger = log.getLogger(name)
20 fmt = \
21 log.Formatter("%(asctime)s %(funcName)s %(levelname)s: %(message)s")
22
23 handler = log.StreamHandler(stream=sys.stdout)
24 handler.setLevel(level)
25 handler.setFormatter(fmt)
26
27 logger.addHandler(handler)
28 logger.setLevel(level)
29
30 return logger
31
32 def valid_ip(self, ip):
33 if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip):
34 return True
35 else:
36 return False
37
38 def valid_url(self, url):
39 p = re.compile(
40 r'^(?:http|ftp)s?://'
41 r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # flake8: noqa
42 r'localhost|'
43 r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
44 r'(?::\d+)?'
45 r'(?:/?|[/?]\S+)$',
46 re.IGNORECASE)
47 if p.match(url):
48 return True
49 else:
50 return False
51
52 def validate_services(self, commands):
53 """Verify the specified services are running on the corresponding
54 service units."""
55 for k, v in commands.iteritems():
56 for cmd in v:
57 output, code = k.run(cmd)
58 if code != 0:
59 return "command `{}` returned {}".format(cmd, str(code))
60 return None
61
62 def _get_config(self, unit, filename):
63 """Get a ConfigParser object for parsing a unit's config file."""
64 file_contents = unit.file_contents(filename)
65 config = ConfigParser.ConfigParser()
66 config.readfp(io.StringIO(file_contents))
67 return config
68
69 def validate_config_data(self, sentry_unit, config_file, section, expected):
70 """Verify that the specified section of the config file contains
71 the expected option key:value pairs."""
72 config = self._get_config(sentry_unit, config_file)
73
74 if section != 'DEFAULT' and not config.has_section(section):
75 return "section [{}] does not exist".format(section)
76
77 for k in expected.keys():
78 if not config.has_option(section, k):
79 return "section [{}] is missing option {}".format(section, k)
80 if config.get(section, k) != expected[k]:
81 return "section [{}] {}:{} != expected {}:{}".format(section,
82 k, config.get(section, k), k, expected[k])
83 return None
84
85 def _validate_dict_data(self, expected, actual):
86 """Compare expected dictionary data vs actual dictionary data.
87 The values in the 'expected' dictionary can be strings, bools, ints,
88 longs, or can be a function that evaluate a variable and returns a
89 bool."""
90 for k, v in expected.iteritems():
91 if k in actual:
92 if isinstance(v, basestring) or \
93 isinstance(v, bool) or \
94 isinstance(v, (int, long)):
95 if v != actual[k]:
96 return "{}:{}".format(k, actual[k])
97 elif not v(actual[k]):
98 return "{}:{}".format(k, actual[k])
99 else:
100 return "key '{}' does not exist".format(k)
101 return None
102
103 def validate_relation_data(self, sentry_unit, relation, expected):
104 """Validate actual relation data based on expected relation data."""
105 actual = sentry_unit.relation(relation[0], relation[1])
106 self.log.debug('actual: {}'.format(repr(actual)))
107 return self._validate_dict_data(expected, actual)
108
109 def _validate_list_data(self, expected, actual):
110 """Compare expected list vs actual list data."""
111 for e in expected:
112 if e not in actual:
113 return "expected item {} not found in actual list".format(e)
114 return None
115
116 def not_null(self, string):
117 if string != None:
118 return True
119 else:
120 return False
121
122 def _get_file_mtime(self, sentry_unit, filename):
123 """Get last modification time of file."""
124 return sentry_unit.file_stat(filename)['mtime']
125
126 def _get_dir_mtime(self, sentry_unit, directory):
127 """Get last modification time of directory."""
128 return sentry_unit.directory_stat(directory)['mtime']
129
130 def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False):
131 """Determine start time of the process based on the last modification
132 time of the /proc/pid directory. If pgrep_full is True, the process
133 name is matched against the full command line."""
134 if pgrep_full:
135 cmd = 'pgrep -o -f {}'.format(service)
136 else:
137 cmd = 'pgrep -o {}'.format(service)
138 proc_dir = '/proc/{}'.format(sentry_unit.run(cmd)[0].strip())
139 return self._get_dir_mtime(sentry_unit, proc_dir)
140
141 def service_restarted(self, sentry_unit, service, filename,
142 pgrep_full=False, sleep_time=20):
143 """Compare a service's start time vs a file's last modification time
144 (such as a config file for that service) to determine if the service
145 has been restarted."""
146 sleep(sleep_time)
147 if self._get_proc_start_time(sentry_unit, service, pgrep_full) >= \
148 self._get_file_mtime(sentry_unit, filename):
149 return True
150 else:
151 return False
152
153 def relation_error(self, name, data):
154 return 'unexpected relation data in {} - {}'.format(name, data)
155
156 def endpoint_error(self, name, data):
157 return 'unexpected endpoint data in {} - {}'.format(name, data)
0158
=== added directory 'tests/charmhelpers/contrib/openstack'
=== added file 'tests/charmhelpers/contrib/openstack/__init__.py'
=== added directory 'tests/charmhelpers/contrib/openstack/amulet'
=== added file 'tests/charmhelpers/contrib/openstack/amulet/__init__.py'
=== added file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 1970-01-01 00:00:00 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2014-07-11 16:43:39 +0000
@@ -0,0 +1,57 @@
1from charmhelpers.contrib.amulet.deployment import (
2 AmuletDeployment
3)
4
5
6class OpenStackAmuletDeployment(AmuletDeployment):
7 """This class inherits from AmuletDeployment and has additional support
8 that is specifically for use by OpenStack charms."""
9
10 def __init__(self, series, openstack=None, source=None):
11 """Initialize the deployment environment."""
12 super(OpenStackAmuletDeployment, self).__init__(series)
13 self.openstack = openstack
14 self.source = source
15
16 def _add_services(self, this_service, other_services):
17 """Add services to the deployment and set openstack-origin."""
18 super(OpenStackAmuletDeployment, self)._add_services(this_service,
19 other_services)
20 name = 0
21 services = other_services
22 services.append(this_service)
23 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph']
24
25 if self.openstack:
26 for svc in services:
27 charm_name = self._get_charm_name(svc[name])
28 if charm_name not in use_source:
29 config = {'openstack-origin': self.openstack}
30 self.d.configure(svc[name], config)
31
32 if self.source:
33 for svc in services:
34 charm_name = self._get_charm_name(svc[name])
35 if charm_name in use_source:
36 config = {'source': self.source}
37 self.d.configure(svc[name], config)
38
39 def _configure_services(self, configs):
40 """Configure all of the services."""
41 for service, config in configs.iteritems():
42 self.d.configure(service, config)
43
44 def _get_openstack_release(self):
45 """Return an integer representing the enum value of the openstack
46 release."""
47 self.precise_essex, self.precise_folsom, self.precise_grizzly, \
48 self.precise_havana, self.precise_icehouse, \
49 self.trusty_icehouse = range(6)
50 releases = {
51 ('precise', None): self.precise_essex,
52 ('precise', 'cloud:precise-folsom'): self.precise_folsom,
53 ('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
54 ('precise', 'cloud:precise-havana'): self.precise_havana,
55 ('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
56 ('trusty', None): self.trusty_icehouse}
57 return releases[(self.series, self.openstack)]
058
=== added file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
--- tests/charmhelpers/contrib/openstack/amulet/utils.py 1970-01-01 00:00:00 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2014-07-11 16:43:39 +0000
@@ -0,0 +1,253 @@
1import logging
2import os
3import time
4import urllib
5
6import glanceclient.v1.client as glance_client
7import keystoneclient.v2_0 as keystone_client
8import novaclient.v1_1.client as nova_client
9
10from charmhelpers.contrib.amulet.utils import (
11 AmuletUtils
12)
13
14DEBUG = logging.DEBUG
15ERROR = logging.ERROR
16
17
18class OpenStackAmuletUtils(AmuletUtils):
19 """This class inherits from AmuletUtils and has additional support
20 that is specifically for use by OpenStack charms."""
21
22 def __init__(self, log_level=ERROR):
23 """Initialize the deployment environment."""
24 super(OpenStackAmuletUtils, self).__init__(log_level)
25
26 def validate_endpoint_data(self, endpoints, admin_port, internal_port,
27 public_port, expected):
28 """Validate actual endpoint data vs expected endpoint data. The ports
29 are used to find the matching endpoint."""
30 found = False
31 for ep in endpoints:
32 self.log.debug('endpoint: {}'.format(repr(ep)))
33 if admin_port in ep.adminurl and internal_port in ep.internalurl \
34 and public_port in ep.publicurl:
35 found = True
36 actual = {'id': ep.id,
37 'region': ep.region,
38 'adminurl': ep.adminurl,
39 'internalurl': ep.internalurl,
40 'publicurl': ep.publicurl,
41 'service_id': ep.service_id}
42 ret = self._validate_dict_data(expected, actual)
43 if ret:
44 return 'unexpected endpoint data - {}'.format(ret)
45
46 if not found:
47 return 'endpoint not found'
48
49 def validate_svc_catalog_endpoint_data(self, expected, actual):
50 """Validate a list of actual service catalog endpoints vs a list of
51 expected service catalog endpoints."""
52 self.log.debug('actual: {}'.format(repr(actual)))
53 for k, v in expected.iteritems():
54 if k in actual:
55 ret = self._validate_dict_data(expected[k][0], actual[k][0])
56 if ret:
57 return self.endpoint_error(k, ret)
58 else:
59 return "endpoint {} does not exist".format(k)
60 return ret
61
62 def validate_tenant_data(self, expected, actual):
63 """Validate a list of actual tenant data vs list of expected tenant
64 data."""
65 self.log.debug('actual: {}'.format(repr(actual)))
66 for e in expected:
67 found = False
68 for act in actual:
69 a = {'enabled': act.enabled, 'description': act.description,
70 'name': act.name, 'id': act.id}
71 if e['name'] == a['name']:
72 found = True
73 ret = self._validate_dict_data(e, a)
74 if ret:
75 return "unexpected tenant data - {}".format(ret)
76 if not found:
77 return "tenant {} does not exist".format(e['name'])
78 return ret
79
80 def validate_role_data(self, expected, actual):
81 """Validate a list of actual role data vs a list of expected role
82 data."""
83 self.log.debug('actual: {}'.format(repr(actual)))
84 for e in expected:
85 found = False
86 for act in actual:
87 a = {'name': act.name, 'id': act.id}
88 if e['name'] == a['name']:
89 found = True
90 ret = self._validate_dict_data(e, a)
91 if ret:
92 return "unexpected role data - {}".format(ret)
93 if not found:
94 return "role {} does not exist".format(e['name'])
95 return ret
96
97 def validate_user_data(self, expected, actual):
98 """Validate a list of actual user data vs a list of expected user
99 data."""
100 self.log.debug('actual: {}'.format(repr(actual)))
101 for e in expected:
102 found = False
103 for act in actual:
104 a = {'enabled': act.enabled, 'name': act.name,
105 'email': act.email, 'tenantId': act.tenantId,
106 'id': act.id}
107 if e['name'] == a['name']:
108 found = True
109 ret = self._validate_dict_data(e, a)
110 if ret:
111 return "unexpected user data - {}".format(ret)
112 if not found:
113 return "user {} does not exist".format(e['name'])
114 return ret
115
116 def validate_flavor_data(self, expected, actual):
117 """Validate a list of actual flavors vs a list of expected flavors."""
118 self.log.debug('actual: {}'.format(repr(actual)))
119 act = [a.name for a in actual]
120 return self._validate_list_data(expected, act)
121
122 def tenant_exists(self, keystone, tenant):
123 """Return True if tenant exists"""
124 return tenant in [t.name for t in keystone.tenants.list()]
125
126 def authenticate_keystone_admin(self, keystone_sentry, user, password,
127 tenant):
128 """Authenticates admin user with the keystone admin endpoint."""
129 service_ip = \
130 keystone_sentry.relation('shared-db',
131 'mysql:shared-db')['private-address']
132 ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
133 return keystone_client.Client(username=user, password=password,
134 tenant_name=tenant, auth_url=ep)
135
136 def authenticate_keystone_user(self, keystone, user, password, tenant):
137 """Authenticates a regular user with the keystone public endpoint."""
138 ep = keystone.service_catalog.url_for(service_type='identity',
139 endpoint_type='publicURL')
140 return keystone_client.Client(username=user, password=password,
141 tenant_name=tenant, auth_url=ep)
142
143 def authenticate_glance_admin(self, keystone):
144 """Authenticates admin user with glance."""
145 ep = keystone.service_catalog.url_for(service_type='image',
146 endpoint_type='adminURL')
147 return glance_client.Client(ep, token=keystone.auth_token)
148
149 def authenticate_nova_user(self, keystone, user, password, tenant):
150 """Authenticates a regular user with nova-api."""
151 ep = keystone.service_catalog.url_for(service_type='identity',
152 endpoint_type='publicURL')
153 return nova_client.Client(username=user, api_key=password,
154 project_id=tenant, auth_url=ep)
155
156 def create_cirros_image(self, glance, image_name):
157 """Download the latest cirros image and upload it to glance."""
158 http_proxy = os.getenv('AMULET_HTTP_PROXY')
159 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
160 if http_proxy:
161 proxies = {'http': http_proxy}
162 opener = urllib.FancyURLopener(proxies)
163 else:
164 opener = urllib.FancyURLopener()
165
166 f = opener.open("http://download.cirros-cloud.net/version/released")
167 version = f.read().strip()
168 cirros_img = "tests/cirros-{}-x86_64-disk.img".format(version)
169
170 if not os.path.exists(cirros_img):
171 cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
172 version, cirros_img)
173 opener.retrieve(cirros_url, cirros_img)
174 f.close()
175
176 with open(cirros_img) as f:
177 image = glance.images.create(name=image_name, is_public=True,
178 disk_format='qcow2',
179 container_format='bare', data=f)
180 count = 1
181 status = image.status
182 while status != 'active' and count < 10:
183 time.sleep(3)
184 image = glance.images.get(image.id)
185 status = image.status
186 self.log.debug('image status: {}'.format(status))
187 count += 1
188
189 if status != 'active':
190 self.log.error('image creation timed out')
191 return None
192
193 return image
194
195 def delete_image(self, glance, image):
196 """Delete the specified image."""
197 num_before = len(list(glance.images.list()))
198 glance.images.delete(image)
199
200 count = 1
201 num_after = len(list(glance.images.list()))
202 while num_after != (num_before - 1) and count < 10:
203 time.sleep(3)
204 num_after = len(list(glance.images.list()))
205 self.log.debug('number of images: {}'.format(num_after))
206 count += 1
207
208 if num_after != (num_before - 1):
209 self.log.error('image deletion timed out')
210 return False
211
212 return True
213
214 def create_instance(self, nova, image_name, instance_name, flavor):
215 """Create the specified instance."""
216 image = nova.images.find(name=image_name)
217 flavor = nova.flavors.find(name=flavor)
218 instance = nova.servers.create(name=instance_name, image=image,
219 flavor=flavor)
220
221 count = 1
222 status = instance.status
223 while status != 'ACTIVE' and count < 60:
224 time.sleep(3)
225 instance = nova.servers.get(instance.id)
226 status = instance.status
227 self.log.debug('instance status: {}'.format(status))
228 count += 1
229
230 if status != 'ACTIVE':
231 self.log.error('instance creation timed out')
232 return None
233
234 return instance
235
236 def delete_instance(self, nova, instance):
237 """Delete the specified instance."""
238 num_before = len(list(nova.servers.list()))
239 nova.servers.delete(instance)
240
241 count = 1
242 num_after = len(list(nova.servers.list()))
243 while num_after != (num_before - 1) and count < 10:
244 time.sleep(3)
245 num_after = len(list(nova.servers.list()))
246 self.log.debug('number of instances: {}'.format(num_after))
247 count += 1
248
249 if num_after != (num_before - 1):
250 self.log.error('instance deletion timed out')
251 return False
252
253 return True

Subscribers

People subscribed via source and target branches