Merge lp:~corey.bryant/charms/trusty/nova-cloud-controller/fix-global-reqs into lp:~openstack-charmers-archive/charms/trusty/nova-cloud-controller/next

Proposed by Corey Bryant
Status: Merged
Approved by: Billy Olsen
Approved revision: 168
Merged at revision: 168
Proposed branch: lp:~corey.bryant/charms/trusty/nova-cloud-controller/fix-global-reqs
Merge into: lp:~openstack-charmers-archive/charms/trusty/nova-cloud-controller/next
Diff against target: 969 lines (+411/-38)
10 files modified
hooks/charmhelpers/contrib/hahelpers/cluster.py (+12/-3)
hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+6/-2)
hooks/charmhelpers/contrib/openstack/amulet/utils.py (+122/-3)
hooks/charmhelpers/contrib/openstack/context.py (+1/-1)
hooks/charmhelpers/contrib/openstack/neutron.py (+6/-4)
hooks/charmhelpers/contrib/openstack/utils.py (+21/-8)
hooks/charmhelpers/core/host.py (+24/-6)
tests/charmhelpers/contrib/amulet/utils.py (+91/-6)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+6/-2)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+122/-3)
To merge this branch: bzr merge lp:~corey.bryant/charms/trusty/nova-cloud-controller/fix-global-reqs
Reviewer Review Type Date Requested Status
Billy Olsen Approve
Review via email: mp+262475@code.launchpad.net

Description of the change

To post a comment you must log in.
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #5476 nova-cloud-controller-next for corey.bryant mp262475
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/5476/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_unit_test #5108 nova-cloud-controller-next for corey.bryant mp262475
    UNIT OK: passed

Build: http://10.245.162.77:8080/job/charm_unit_test/5108/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #4683 nova-cloud-controller-next for corey.bryant mp262475
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/4683/

Revision history for this message
Billy Olsen (billy-olsen) wrote :

LGTM, Approved

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'hooks/charmhelpers/contrib/hahelpers/cluster.py'
--- hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-06-10 20:32:48 +0000
+++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-06-19 16:16:16 +0000
@@ -64,6 +64,10 @@
64 pass64 pass
6565
6666
67class CRMDCNotFound(Exception):
68 pass
69
70
67def is_elected_leader(resource):71def is_elected_leader(resource):
68 """72 """
69 Returns True if the charm executing this is the elected cluster leader.73 Returns True if the charm executing this is the elected cluster leader.
@@ -116,8 +120,9 @@
116 status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)120 status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
117 if not isinstance(status, six.text_type):121 if not isinstance(status, six.text_type):
118 status = six.text_type(status, "utf-8")122 status = six.text_type(status, "utf-8")
119 except subprocess.CalledProcessError:123 except subprocess.CalledProcessError as ex:
120 return False124 raise CRMDCNotFound(str(ex))
125
121 current_dc = ''126 current_dc = ''
122 for line in status.split('\n'):127 for line in status.split('\n'):
123 if line.startswith('Current DC'):128 if line.startswith('Current DC'):
@@ -125,10 +130,14 @@
125 current_dc = line.split(':')[1].split()[0]130 current_dc = line.split(':')[1].split()[0]
126 if current_dc == get_unit_hostname():131 if current_dc == get_unit_hostname():
127 return True132 return True
133 elif current_dc == 'NONE':
134 raise CRMDCNotFound('Current DC: NONE')
135
128 return False136 return False
129137
130138
131@retry_on_exception(5, base_delay=2, exc_type=CRMResourceNotFound)139@retry_on_exception(5, base_delay=2,
140 exc_type=(CRMResourceNotFound, CRMDCNotFound))
132def is_crm_leader(resource, retry=False):141def is_crm_leader(resource, retry=False):
133 """142 """
134 Returns True if the charm calling this is the elected corosync leader,143 Returns True if the charm calling this is the elected corosync leader,
135144
=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-10 15:48:34 +0000
+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-19 16:16:16 +0000
@@ -110,7 +110,8 @@
110 (self.precise_essex, self.precise_folsom, self.precise_grizzly,110 (self.precise_essex, self.precise_folsom, self.precise_grizzly,
111 self.precise_havana, self.precise_icehouse,111 self.precise_havana, self.precise_icehouse,
112 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,112 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
113 self.trusty_kilo, self.vivid_kilo) = range(10)113 self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,
114 self.wily_liberty) = range(12)
114115
115 releases = {116 releases = {
116 ('precise', None): self.precise_essex,117 ('precise', None): self.precise_essex,
@@ -121,8 +122,10 @@
121 ('trusty', None): self.trusty_icehouse,122 ('trusty', None): self.trusty_icehouse,
122 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,123 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
123 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,124 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
125 ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
124 ('utopic', None): self.utopic_juno,126 ('utopic', None): self.utopic_juno,
125 ('vivid', None): self.vivid_kilo}127 ('vivid', None): self.vivid_kilo,
128 ('wily', None): self.wily_liberty}
126 return releases[(self.series, self.openstack)]129 return releases[(self.series, self.openstack)]
127130
128 def _get_openstack_release_string(self):131 def _get_openstack_release_string(self):
@@ -138,6 +141,7 @@
138 ('trusty', 'icehouse'),141 ('trusty', 'icehouse'),
139 ('utopic', 'juno'),142 ('utopic', 'juno'),
140 ('vivid', 'kilo'),143 ('vivid', 'kilo'),
144 ('wily', 'liberty'),
141 ])145 ])
142 if self.openstack:146 if self.openstack:
143 os_origin = self.openstack.split(':')[1]147 os_origin = self.openstack.split(':')[1]
144148
=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/utils.py'
--- hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-03-31 14:56:11 +0000
+++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-06-19 16:16:16 +0000
@@ -16,15 +16,15 @@
1616
17import logging17import logging
18import os18import os
19import six
19import time20import time
20import urllib21import urllib
2122
22import glanceclient.v1.client as glance_client23import glanceclient.v1.client as glance_client
24import heatclient.v1.client as heat_client
23import keystoneclient.v2_0 as keystone_client25import keystoneclient.v2_0 as keystone_client
24import novaclient.v1_1.client as nova_client26import novaclient.v1_1.client as nova_client
2527
26import six
27
28from charmhelpers.contrib.amulet.utils import (28from charmhelpers.contrib.amulet.utils import (
29 AmuletUtils29 AmuletUtils
30)30)
@@ -37,7 +37,7 @@
37 """OpenStack amulet utilities.37 """OpenStack amulet utilities.
3838
39 This class inherits from AmuletUtils and has additional support39 This class inherits from AmuletUtils and has additional support
40 that is specifically for use by OpenStack charms.40 that is specifically for use by OpenStack charm tests.
41 """41 """
4242
43 def __init__(self, log_level=ERROR):43 def __init__(self, log_level=ERROR):
@@ -51,6 +51,8 @@
51 Validate actual endpoint data vs expected endpoint data. The ports51 Validate actual endpoint data vs expected endpoint data. The ports
52 are used to find the matching endpoint.52 are used to find the matching endpoint.
53 """53 """
54 self.log.debug('Validating endpoint data...')
55 self.log.debug('actual: {}'.format(repr(endpoints)))
54 found = False56 found = False
55 for ep in endpoints:57 for ep in endpoints:
56 self.log.debug('endpoint: {}'.format(repr(ep)))58 self.log.debug('endpoint: {}'.format(repr(ep)))
@@ -77,6 +79,7 @@
77 Validate a list of actual service catalog endpoints vs a list of79 Validate a list of actual service catalog endpoints vs a list of
78 expected service catalog endpoints.80 expected service catalog endpoints.
79 """81 """
82 self.log.debug('Validating service catalog endpoint data...')
80 self.log.debug('actual: {}'.format(repr(actual)))83 self.log.debug('actual: {}'.format(repr(actual)))
81 for k, v in six.iteritems(expected):84 for k, v in six.iteritems(expected):
82 if k in actual:85 if k in actual:
@@ -93,6 +96,7 @@
93 Validate a list of actual tenant data vs list of expected tenant96 Validate a list of actual tenant data vs list of expected tenant
94 data.97 data.
95 """98 """
99 self.log.debug('Validating tenant data...')
96 self.log.debug('actual: {}'.format(repr(actual)))100 self.log.debug('actual: {}'.format(repr(actual)))
97 for e in expected:101 for e in expected:
98 found = False102 found = False
@@ -114,6 +118,7 @@
114 Validate a list of actual role data vs a list of expected role118 Validate a list of actual role data vs a list of expected role
115 data.119 data.
116 """120 """
121 self.log.debug('Validating role data...')
117 self.log.debug('actual: {}'.format(repr(actual)))122 self.log.debug('actual: {}'.format(repr(actual)))
118 for e in expected:123 for e in expected:
119 found = False124 found = False
@@ -134,6 +139,7 @@
134 Validate a list of actual user data vs a list of expected user139 Validate a list of actual user data vs a list of expected user
135 data.140 data.
136 """141 """
142 self.log.debug('Validating user data...')
137 self.log.debug('actual: {}'.format(repr(actual)))143 self.log.debug('actual: {}'.format(repr(actual)))
138 for e in expected:144 for e in expected:
139 found = False145 found = False
@@ -155,17 +161,20 @@
155161
156 Validate a list of actual flavors vs a list of expected flavors.162 Validate a list of actual flavors vs a list of expected flavors.
157 """163 """
164 self.log.debug('Validating flavor data...')
158 self.log.debug('actual: {}'.format(repr(actual)))165 self.log.debug('actual: {}'.format(repr(actual)))
159 act = [a.name for a in actual]166 act = [a.name for a in actual]
160 return self._validate_list_data(expected, act)167 return self._validate_list_data(expected, act)
161168
162 def tenant_exists(self, keystone, tenant):169 def tenant_exists(self, keystone, tenant):
163 """Return True if tenant exists."""170 """Return True if tenant exists."""
171 self.log.debug('Checking if tenant exists ({})...'.format(tenant))
164 return tenant in [t.name for t in keystone.tenants.list()]172 return tenant in [t.name for t in keystone.tenants.list()]
165173
166 def authenticate_keystone_admin(self, keystone_sentry, user, password,174 def authenticate_keystone_admin(self, keystone_sentry, user, password,
167 tenant):175 tenant):
168 """Authenticates admin user with the keystone admin endpoint."""176 """Authenticates admin user with the keystone admin endpoint."""
177 self.log.debug('Authenticating keystone admin...')
169 unit = keystone_sentry178 unit = keystone_sentry
170 service_ip = unit.relation('shared-db',179 service_ip = unit.relation('shared-db',
171 'mysql:shared-db')['private-address']180 'mysql:shared-db')['private-address']
@@ -175,6 +184,7 @@
175184
176 def authenticate_keystone_user(self, keystone, user, password, tenant):185 def authenticate_keystone_user(self, keystone, user, password, tenant):
177 """Authenticates a regular user with the keystone public endpoint."""186 """Authenticates a regular user with the keystone public endpoint."""
187 self.log.debug('Authenticating keystone user ({})...'.format(user))
178 ep = keystone.service_catalog.url_for(service_type='identity',188 ep = keystone.service_catalog.url_for(service_type='identity',
179 endpoint_type='publicURL')189 endpoint_type='publicURL')
180 return keystone_client.Client(username=user, password=password,190 return keystone_client.Client(username=user, password=password,
@@ -182,12 +192,21 @@
182192
183 def authenticate_glance_admin(self, keystone):193 def authenticate_glance_admin(self, keystone):
184 """Authenticates admin user with glance."""194 """Authenticates admin user with glance."""
195 self.log.debug('Authenticating glance admin...')
185 ep = keystone.service_catalog.url_for(service_type='image',196 ep = keystone.service_catalog.url_for(service_type='image',
186 endpoint_type='adminURL')197 endpoint_type='adminURL')
187 return glance_client.Client(ep, token=keystone.auth_token)198 return glance_client.Client(ep, token=keystone.auth_token)
188199
200 def authenticate_heat_admin(self, keystone):
201 """Authenticates the admin user with heat."""
202 self.log.debug('Authenticating heat admin...')
203 ep = keystone.service_catalog.url_for(service_type='orchestration',
204 endpoint_type='publicURL')
205 return heat_client.Client(endpoint=ep, token=keystone.auth_token)
206
189 def authenticate_nova_user(self, keystone, user, password, tenant):207 def authenticate_nova_user(self, keystone, user, password, tenant):
190 """Authenticates a regular user with nova-api."""208 """Authenticates a regular user with nova-api."""
209 self.log.debug('Authenticating nova user ({})...'.format(user))
191 ep = keystone.service_catalog.url_for(service_type='identity',210 ep = keystone.service_catalog.url_for(service_type='identity',
192 endpoint_type='publicURL')211 endpoint_type='publicURL')
193 return nova_client.Client(username=user, api_key=password,212 return nova_client.Client(username=user, api_key=password,
@@ -195,6 +214,7 @@
195214
196 def create_cirros_image(self, glance, image_name):215 def create_cirros_image(self, glance, image_name):
197 """Download the latest cirros image and upload it to glance."""216 """Download the latest cirros image and upload it to glance."""
217 self.log.debug('Creating glance image ({})...'.format(image_name))
198 http_proxy = os.getenv('AMULET_HTTP_PROXY')218 http_proxy = os.getenv('AMULET_HTTP_PROXY')
199 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))219 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
200 if http_proxy:220 if http_proxy:
@@ -235,6 +255,11 @@
235255
236 def delete_image(self, glance, image):256 def delete_image(self, glance, image):
237 """Delete the specified image."""257 """Delete the specified image."""
258
259 # /!\ DEPRECATION WARNING
260 self.log.warn('/!\\ DEPRECATION WARNING: use '
261 'delete_resource instead of delete_image.')
262 self.log.debug('Deleting glance image ({})...'.format(image))
238 num_before = len(list(glance.images.list()))263 num_before = len(list(glance.images.list()))
239 glance.images.delete(image)264 glance.images.delete(image)
240265
@@ -254,6 +279,8 @@
254279
255 def create_instance(self, nova, image_name, instance_name, flavor):280 def create_instance(self, nova, image_name, instance_name, flavor):
256 """Create the specified instance."""281 """Create the specified instance."""
282 self.log.debug('Creating instance '
283 '({}|{}|{})'.format(instance_name, image_name, flavor))
257 image = nova.images.find(name=image_name)284 image = nova.images.find(name=image_name)
258 flavor = nova.flavors.find(name=flavor)285 flavor = nova.flavors.find(name=flavor)
259 instance = nova.servers.create(name=instance_name, image=image,286 instance = nova.servers.create(name=instance_name, image=image,
@@ -276,6 +303,11 @@
276303
277 def delete_instance(self, nova, instance):304 def delete_instance(self, nova, instance):
278 """Delete the specified instance."""305 """Delete the specified instance."""
306
307 # /!\ DEPRECATION WARNING
308 self.log.warn('/!\\ DEPRECATION WARNING: use '
309 'delete_resource instead of delete_instance.')
310 self.log.debug('Deleting instance ({})...'.format(instance))
279 num_before = len(list(nova.servers.list()))311 num_before = len(list(nova.servers.list()))
280 nova.servers.delete(instance)312 nova.servers.delete(instance)
281313
@@ -292,3 +324,90 @@
292 return False324 return False
293325
294 return True326 return True
327
328 def create_or_get_keypair(self, nova, keypair_name="testkey"):
329 """Create a new keypair, or return pointer if it already exists."""
330 try:
331 _keypair = nova.keypairs.get(keypair_name)
332 self.log.debug('Keypair ({}) already exists, '
333 'using it.'.format(keypair_name))
334 return _keypair
335 except:
336 self.log.debug('Keypair ({}) does not exist, '
337 'creating it.'.format(keypair_name))
338
339 _keypair = nova.keypairs.create(name=keypair_name)
340 return _keypair
341
342 def delete_resource(self, resource, resource_id,
343 msg="resource", max_wait=120):
344 """Delete one openstack resource, such as one instance, keypair,
345 image, volume, stack, etc., and confirm deletion within max wait time.
346
347 :param resource: pointer to os resource type, ex:glance_client.images
348 :param resource_id: unique name or id for the openstack resource
349 :param msg: text to identify purpose in logging
350 :param max_wait: maximum wait time in seconds
351 :returns: True if successful, otherwise False
352 """
353 num_before = len(list(resource.list()))
354 resource.delete(resource_id)
355
356 tries = 0
357 num_after = len(list(resource.list()))
358 while num_after != (num_before - 1) and tries < (max_wait / 4):
359 self.log.debug('{} delete check: '
360 '{} [{}:{}] {}'.format(msg, tries,
361 num_before,
362 num_after,
363 resource_id))
364 time.sleep(4)
365 num_after = len(list(resource.list()))
366 tries += 1
367
368 self.log.debug('{}: expected, actual count = {}, '
369 '{}'.format(msg, num_before - 1, num_after))
370
371 if num_after == (num_before - 1):
372 return True
373 else:
374 self.log.error('{} delete timed out'.format(msg))
375 return False
376
377 def resource_reaches_status(self, resource, resource_id,
378 expected_stat='available',
379 msg='resource', max_wait=120):
380 """Wait for an openstack resources status to reach an
381 expected status within a specified time. Useful to confirm that
382 nova instances, cinder vols, snapshots, glance images, heat stacks
383 and other resources eventually reach the expected status.
384
385 :param resource: pointer to os resource type, ex: heat_client.stacks
386 :param resource_id: unique id for the openstack resource
387 :param expected_stat: status to expect resource to reach
388 :param msg: text to identify purpose in logging
389 :param max_wait: maximum wait time in seconds
390 :returns: True if successful, False if status is not reached
391 """
392
393 tries = 0
394 resource_stat = resource.get(resource_id).status
395 while resource_stat != expected_stat and tries < (max_wait / 4):
396 self.log.debug('{} status check: '
397 '{} [{}:{}] {}'.format(msg, tries,
398 resource_stat,
399 expected_stat,
400 resource_id))
401 time.sleep(4)
402 resource_stat = resource.get(resource_id).status
403 tries += 1
404
405 self.log.debug('{}: expected, actual status = {}, '
406 '{}'.format(msg, resource_stat, expected_stat))
407
408 if resource_stat == expected_stat:
409 return True
410 else:
411 self.log.debug('{} never reached expected status: '
412 '{}'.format(resource_id, expected_stat))
413 return False
295414
=== modified file 'hooks/charmhelpers/contrib/openstack/context.py'
--- hooks/charmhelpers/contrib/openstack/context.py 2015-06-10 15:48:34 +0000
+++ hooks/charmhelpers/contrib/openstack/context.py 2015-06-19 16:16:16 +0000
@@ -240,7 +240,7 @@
240 if self.relation_prefix:240 if self.relation_prefix:
241 password_setting = self.relation_prefix + '_password'241 password_setting = self.relation_prefix + '_password'
242242
243 for rid in relation_ids('shared-db'):243 for rid in relation_ids(self.interfaces[0]):
244 for unit in related_units(rid):244 for unit in related_units(rid):
245 rdata = relation_get(rid=rid, unit=unit)245 rdata = relation_get(rid=rid, unit=unit)
246 host = rdata.get('db_host')246 host = rdata.get('db_host')
247247
=== modified file 'hooks/charmhelpers/contrib/openstack/neutron.py'
--- hooks/charmhelpers/contrib/openstack/neutron.py 2015-06-10 20:32:48 +0000
+++ hooks/charmhelpers/contrib/openstack/neutron.py 2015-06-19 16:16:16 +0000
@@ -172,14 +172,16 @@
172 'services': ['calico-felix',172 'services': ['calico-felix',
173 'bird',173 'bird',
174 'neutron-dhcp-agent',174 'neutron-dhcp-agent',
175 'nova-api-metadata'],175 'nova-api-metadata',
176 'etcd'],
176 'packages': [[headers_package()] + determine_dkms_package(),177 'packages': [[headers_package()] + determine_dkms_package(),
177 ['calico-compute',178 ['calico-compute',
178 'bird',179 'bird',
179 'neutron-dhcp-agent',180 'neutron-dhcp-agent',
180 'nova-api-metadata']],181 'nova-api-metadata',
181 'server_packages': ['neutron-server', 'calico-control'],182 'etcd']],
182 'server_services': ['neutron-server']183 'server_packages': ['neutron-server', 'calico-control', 'etcd'],
184 'server_services': ['neutron-server', 'etcd']
183 },185 },
184 'vsp': {186 'vsp': {
185 'config': '/etc/neutron/plugins/nuage/nuage_plugin.ini',187 'config': '/etc/neutron/plugins/nuage/nuage_plugin.ini',
186188
=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
--- hooks/charmhelpers/contrib/openstack/utils.py 2015-06-11 13:33:42 +0000
+++ hooks/charmhelpers/contrib/openstack/utils.py 2015-06-19 16:16:16 +0000
@@ -79,6 +79,7 @@
79 ('trusty', 'icehouse'),79 ('trusty', 'icehouse'),
80 ('utopic', 'juno'),80 ('utopic', 'juno'),
81 ('vivid', 'kilo'),81 ('vivid', 'kilo'),
82 ('wily', 'liberty'),
82])83])
8384
8485
@@ -91,6 +92,7 @@
91 ('2014.1', 'icehouse'),92 ('2014.1', 'icehouse'),
92 ('2014.2', 'juno'),93 ('2014.2', 'juno'),
93 ('2015.1', 'kilo'),94 ('2015.1', 'kilo'),
95 ('2015.2', 'liberty'),
94])96])
9597
96# The ugly duckling98# The ugly duckling
@@ -113,6 +115,7 @@
113 ('2.2.0', 'juno'),115 ('2.2.0', 'juno'),
114 ('2.2.1', 'kilo'),116 ('2.2.1', 'kilo'),
115 ('2.2.2', 'kilo'),117 ('2.2.2', 'kilo'),
118 ('2.3.0', 'liberty'),
116])119])
117120
118DEFAULT_LOOPBACK_SIZE = '5G'121DEFAULT_LOOPBACK_SIZE = '5G'
@@ -321,6 +324,9 @@
321 'kilo': 'trusty-updates/kilo',324 'kilo': 'trusty-updates/kilo',
322 'kilo/updates': 'trusty-updates/kilo',325 'kilo/updates': 'trusty-updates/kilo',
323 'kilo/proposed': 'trusty-proposed/kilo',326 'kilo/proposed': 'trusty-proposed/kilo',
327 'liberty': 'trusty-updates/liberty',
328 'liberty/updates': 'trusty-updates/liberty',
329 'liberty/proposed': 'trusty-proposed/liberty',
324 }330 }
325331
326 try:332 try:
@@ -549,6 +555,11 @@
549555
550 pip_create_virtualenv(os.path.join(parent_dir, 'venv'))556 pip_create_virtualenv(os.path.join(parent_dir, 'venv'))
551557
558 # Upgrade setuptools from default virtualenv version. The default version
559 # in trusty breaks update.py in global requirements master branch.
560 pip_install('setuptools', upgrade=True, proxy=http_proxy,
561 venv=os.path.join(parent_dir, 'venv'))
562
552 for p in projects['repositories']:563 for p in projects['repositories']:
553 repo = p['repository']564 repo = p['repository']
554 branch = p['branch']565 branch = p['branch']
@@ -610,24 +621,24 @@
610 else:621 else:
611 repo_dir = dest_dir622 repo_dir = dest_dir
612623
624 venv = os.path.join(parent_dir, 'venv')
625
613 if update_requirements:626 if update_requirements:
614 if not requirements_dir:627 if not requirements_dir:
615 error_out('requirements repo must be cloned before '628 error_out('requirements repo must be cloned before '
616 'updating from global requirements.')629 'updating from global requirements.')
617 _git_update_requirements(repo_dir, requirements_dir)630 _git_update_requirements(venv, repo_dir, requirements_dir)
618631
619 juju_log('Installing git repo from dir: {}'.format(repo_dir))632 juju_log('Installing git repo from dir: {}'.format(repo_dir))
620 if http_proxy:633 if http_proxy:
621 pip_install(repo_dir, proxy=http_proxy,634 pip_install(repo_dir, proxy=http_proxy, venv=venv)
622 venv=os.path.join(parent_dir, 'venv'))
623 else:635 else:
624 pip_install(repo_dir,636 pip_install(repo_dir, venv=venv)
625 venv=os.path.join(parent_dir, 'venv'))
626637
627 return repo_dir638 return repo_dir
628639
629640
630def _git_update_requirements(package_dir, reqs_dir):641def _git_update_requirements(venv, package_dir, reqs_dir):
631 """642 """
632 Update from global requirements.643 Update from global requirements.
633644
@@ -636,12 +647,14 @@
636 """647 """
637 orig_dir = os.getcwd()648 orig_dir = os.getcwd()
638 os.chdir(reqs_dir)649 os.chdir(reqs_dir)
639 cmd = ['python', 'update.py', package_dir]650 python = os.path.join(venv, 'bin/python')
651 cmd = [python, 'update.py', package_dir]
640 try:652 try:
641 subprocess.check_call(cmd)653 subprocess.check_call(cmd)
642 except subprocess.CalledProcessError:654 except subprocess.CalledProcessError:
643 package = os.path.basename(package_dir)655 package = os.path.basename(package_dir)
644 error_out("Error updating {} from global-requirements.txt".format(package))656 error_out("Error updating {} from "
657 "global-requirements.txt".format(package))
645 os.chdir(orig_dir)658 os.chdir(orig_dir)
646659
647660
648661
=== modified file 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py 2015-06-10 20:32:48 +0000
+++ hooks/charmhelpers/core/host.py 2015-06-19 16:16:16 +0000
@@ -24,6 +24,7 @@
24import os24import os
25import re25import re
26import pwd26import pwd
27import glob
27import grp28import grp
28import random29import random
29import string30import string
@@ -269,6 +270,21 @@
269 return None270 return None
270271
271272
273def path_hash(path):
274 """
275 Generate a hash checksum of all files matching 'path'. Standard wildcards
276 like '*' and '?' are supported, see documentation for the 'glob' module for
277 more information.
278
279 :return: dict: A { filename: hash } dictionary for all matched files.
280 Empty if none found.
281 """
282 return {
283 filename: file_hash(filename)
284 for filename in glob.iglob(path)
285 }
286
287
272def check_hash(path, checksum, hash_type='md5'):288def check_hash(path, checksum, hash_type='md5'):
273 """289 """
274 Validate a file using a cryptographic checksum.290 Validate a file using a cryptographic checksum.
@@ -296,23 +312,25 @@
296312
297 @restart_on_change({313 @restart_on_change({
298 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]314 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
315 '/etc/apache/sites-enabled/*': [ 'apache2' ]
299 })316 })
300 def ceph_client_changed():317 def config_changed():
301 pass # your code here318 pass # your code here
302319
303 In this example, the cinder-api and cinder-volume services320 In this example, the cinder-api and cinder-volume services
304 would be restarted if /etc/ceph/ceph.conf is changed by the321 would be restarted if /etc/ceph/ceph.conf is changed by the
305 ceph_client_changed function.322 ceph_client_changed function. The apache2 service would be
323 restarted if any file matching the pattern got changed, created
324 or removed. Standard wildcards are supported, see documentation
325 for the 'glob' module for more information.
306 """326 """
307 def wrap(f):327 def wrap(f):
308 def wrapped_f(*args, **kwargs):328 def wrapped_f(*args, **kwargs):
309 checksums = {}329 checksums = {path: path_hash(path) for path in restart_map}
310 for path in restart_map:
311 checksums[path] = file_hash(path)
312 f(*args, **kwargs)330 f(*args, **kwargs)
313 restarts = []331 restarts = []
314 for path in restart_map:332 for path in restart_map:
315 if checksums[path] != file_hash(path):333 if path_hash(path) != checksums[path]:
316 restarts += restart_map[path]334 restarts += restart_map[path]
317 services_list = list(OrderedDict.fromkeys(restarts))335 services_list = list(OrderedDict.fromkeys(restarts))
318 if not stopstart:336 if not stopstart:
319337
=== modified file 'tests/charmhelpers/contrib/amulet/utils.py'
--- tests/charmhelpers/contrib/amulet/utils.py 2015-05-12 13:21:27 +0000
+++ tests/charmhelpers/contrib/amulet/utils.py 2015-06-19 16:16:16 +0000
@@ -15,13 +15,15 @@
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1616
17import ConfigParser17import ConfigParser
18import distro_info
18import io19import io
19import logging20import logging
21import os
20import re22import re
23import six
21import sys24import sys
22import time25import time
2326import urlparse
24import six
2527
2628
27class AmuletUtils(object):29class AmuletUtils(object):
@@ -33,6 +35,7 @@
3335
34 def __init__(self, log_level=logging.ERROR):36 def __init__(self, log_level=logging.ERROR):
35 self.log = self.get_logger(level=log_level)37 self.log = self.get_logger(level=log_level)
38 self.ubuntu_releases = self.get_ubuntu_releases()
3639
37 def get_logger(self, name="amulet-logger", level=logging.DEBUG):40 def get_logger(self, name="amulet-logger", level=logging.DEBUG):
38 """Get a logger object that will log to stdout."""41 """Get a logger object that will log to stdout."""
@@ -70,12 +73,44 @@
70 else:73 else:
71 return False74 return False
7275
76 def get_ubuntu_release_from_sentry(self, sentry_unit):
77 """Get Ubuntu release codename from sentry unit.
78
79 :param sentry_unit: amulet sentry/service unit pointer
80 :returns: list of strings - release codename, failure message
81 """
82 msg = None
83 cmd = 'lsb_release -cs'
84 release, code = sentry_unit.run(cmd)
85 if code == 0:
86 self.log.debug('{} lsb_release: {}'.format(
87 sentry_unit.info['unit_name'], release))
88 else:
89 msg = ('{} `{}` returned {} '
90 '{}'.format(sentry_unit.info['unit_name'],
91 cmd, release, code))
92 if release not in self.ubuntu_releases:
93 msg = ("Release ({}) not found in Ubuntu releases "
94 "({})".format(release, self.ubuntu_releases))
95 return release, msg
96
73 def validate_services(self, commands):97 def validate_services(self, commands):
74 """Validate services.98 """Validate that lists of commands succeed on service units. Can be
7599 used to verify system services are running on the corresponding
76 Verify the specified services are running on the corresponding
77 service units.100 service units.
78 """101
102 :param commands: dict with sentry keys and arbitrary command list vals
103 :returns: None if successful, Failure string message otherwise
104 """
105 self.log.debug('Checking status of system services...')
106
107 # /!\ DEPRECATION WARNING (beisner):
108 # New and existing tests should be rewritten to use
109 # validate_services_by_name() as it is aware of init systems.
110 self.log.warn('/!\\ DEPRECATION WARNING: use '
111 'validate_services_by_name instead of validate_services '
112 'due to init system differences.')
113
79 for k, v in six.iteritems(commands):114 for k, v in six.iteritems(commands):
80 for cmd in v:115 for cmd in v:
81 output, code = k.run(cmd)116 output, code = k.run(cmd)
@@ -86,6 +121,41 @@
86 return "command `{}` returned {}".format(cmd, str(code))121 return "command `{}` returned {}".format(cmd, str(code))
87 return None122 return None
88123
124 def validate_services_by_name(self, sentry_services):
125 """Validate system service status by service name, automatically
126 detecting init system based on Ubuntu release codename.
127
128 :param sentry_services: dict with sentry keys and svc list values
129 :returns: None if successful, Failure string message otherwise
130 """
131 self.log.debug('Checking status of system services...')
132
133 # Point at which systemd became a thing
134 systemd_switch = self.ubuntu_releases.index('vivid')
135
136 for sentry_unit, services_list in six.iteritems(sentry_services):
137 # Get lsb_release codename from unit
138 release, ret = self.get_ubuntu_release_from_sentry(sentry_unit)
139 if ret:
140 return ret
141
142 for service_name in services_list:
143 if (self.ubuntu_releases.index(release) >= systemd_switch or
144 service_name == "rabbitmq-server"):
145 # init is systemd
146 cmd = 'sudo service {} status'.format(service_name)
147 elif self.ubuntu_releases.index(release) < systemd_switch:
148 # init is upstart
149 cmd = 'sudo status {}'.format(service_name)
150
151 output, code = sentry_unit.run(cmd)
152 self.log.debug('{} `{}` returned '
153 '{}'.format(sentry_unit.info['unit_name'],
154 cmd, code))
155 if code != 0:
156 return "command `{}` returned {}".format(cmd, str(code))
157 return None
158
89 def _get_config(self, unit, filename):159 def _get_config(self, unit, filename):
90 """Get a ConfigParser object for parsing a unit's config file."""160 """Get a ConfigParser object for parsing a unit's config file."""
91 file_contents = unit.file_contents(filename)161 file_contents = unit.file_contents(filename)
@@ -104,6 +174,9 @@
104 Verify that the specified section of the config file contains174 Verify that the specified section of the config file contains
105 the expected option key:value pairs.175 the expected option key:value pairs.
106 """176 """
177 self.log.debug('Validating config file data ({} in {} on {})'
178 '...'.format(section, config_file,
179 sentry_unit.info['unit_name']))
107 config = self._get_config(sentry_unit, config_file)180 config = self._get_config(sentry_unit, config_file)
108181
109 if section != 'DEFAULT' and not config.has_section(section):182 if section != 'DEFAULT' and not config.has_section(section):
@@ -321,3 +394,15 @@
321394
322 def endpoint_error(self, name, data):395 def endpoint_error(self, name, data):
323 return 'unexpected endpoint data in {} - {}'.format(name, data)396 return 'unexpected endpoint data in {} - {}'.format(name, data)
397
398 def get_ubuntu_releases(self):
399 """Return a list of all Ubuntu releases in order of release."""
400 _d = distro_info.UbuntuDistroInfo()
401 _release_list = _d.all
402 self.log.debug('Ubuntu release list: {}'.format(_release_list))
403 return _release_list
404
405 def file_to_url(self, file_rel_path):
406 """Convert a relative file path to a file URL."""
407 _abs_path = os.path.abspath(file_rel_path)
408 return urlparse.urlparse(_abs_path, scheme='file').geturl()
324409
=== modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-10 15:48:34 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-19 16:16:16 +0000
@@ -110,7 +110,8 @@
110 (self.precise_essex, self.precise_folsom, self.precise_grizzly,110 (self.precise_essex, self.precise_folsom, self.precise_grizzly,
111 self.precise_havana, self.precise_icehouse,111 self.precise_havana, self.precise_icehouse,
112 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,112 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
113 self.trusty_kilo, self.vivid_kilo) = range(10)113 self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,
114 self.wily_liberty) = range(12)
114115
115 releases = {116 releases = {
116 ('precise', None): self.precise_essex,117 ('precise', None): self.precise_essex,
@@ -121,8 +122,10 @@
121 ('trusty', None): self.trusty_icehouse,122 ('trusty', None): self.trusty_icehouse,
122 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,123 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
123 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,124 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
125 ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
124 ('utopic', None): self.utopic_juno,126 ('utopic', None): self.utopic_juno,
125 ('vivid', None): self.vivid_kilo}127 ('vivid', None): self.vivid_kilo,
128 ('wily', None): self.wily_liberty}
126 return releases[(self.series, self.openstack)]129 return releases[(self.series, self.openstack)]
127130
128 def _get_openstack_release_string(self):131 def _get_openstack_release_string(self):
@@ -138,6 +141,7 @@
138 ('trusty', 'icehouse'),141 ('trusty', 'icehouse'),
139 ('utopic', 'juno'),142 ('utopic', 'juno'),
140 ('vivid', 'kilo'),143 ('vivid', 'kilo'),
144 ('wily', 'liberty'),
141 ])145 ])
142 if self.openstack:146 if self.openstack:
143 os_origin = self.openstack.split(':')[1]147 os_origin = self.openstack.split(':')[1]
144148
=== modified file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
--- tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-03-31 11:39:19 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-06-19 16:16:16 +0000
@@ -16,15 +16,15 @@
1616
17import logging17import logging
18import os18import os
19import six
19import time20import time
20import urllib21import urllib
2122
22import glanceclient.v1.client as glance_client23import glanceclient.v1.client as glance_client
24import heatclient.v1.client as heat_client
23import keystoneclient.v2_0 as keystone_client25import keystoneclient.v2_0 as keystone_client
24import novaclient.v1_1.client as nova_client26import novaclient.v1_1.client as nova_client
2527
26import six
27
28from charmhelpers.contrib.amulet.utils import (28from charmhelpers.contrib.amulet.utils import (
29 AmuletUtils29 AmuletUtils
30)30)
@@ -37,7 +37,7 @@
37 """OpenStack amulet utilities.37 """OpenStack amulet utilities.
3838
39 This class inherits from AmuletUtils and has additional support39 This class inherits from AmuletUtils and has additional support
40 that is specifically for use by OpenStack charms.40 that is specifically for use by OpenStack charm tests.
41 """41 """
4242
43 def __init__(self, log_level=ERROR):43 def __init__(self, log_level=ERROR):
@@ -51,6 +51,8 @@
51 Validate actual endpoint data vs expected endpoint data. The ports51 Validate actual endpoint data vs expected endpoint data. The ports
52 are used to find the matching endpoint.52 are used to find the matching endpoint.
53 """53 """
54 self.log.debug('Validating endpoint data...')
55 self.log.debug('actual: {}'.format(repr(endpoints)))
54 found = False56 found = False
55 for ep in endpoints:57 for ep in endpoints:
56 self.log.debug('endpoint: {}'.format(repr(ep)))58 self.log.debug('endpoint: {}'.format(repr(ep)))
@@ -77,6 +79,7 @@
77 Validate a list of actual service catalog endpoints vs a list of79 Validate a list of actual service catalog endpoints vs a list of
78 expected service catalog endpoints.80 expected service catalog endpoints.
79 """81 """
82 self.log.debug('Validating service catalog endpoint data...')
80 self.log.debug('actual: {}'.format(repr(actual)))83 self.log.debug('actual: {}'.format(repr(actual)))
81 for k, v in six.iteritems(expected):84 for k, v in six.iteritems(expected):
82 if k in actual:85 if k in actual:
@@ -93,6 +96,7 @@
93 Validate a list of actual tenant data vs list of expected tenant96 Validate a list of actual tenant data vs list of expected tenant
94 data.97 data.
95 """98 """
99 self.log.debug('Validating tenant data...')
96 self.log.debug('actual: {}'.format(repr(actual)))100 self.log.debug('actual: {}'.format(repr(actual)))
97 for e in expected:101 for e in expected:
98 found = False102 found = False
@@ -114,6 +118,7 @@
114 Validate a list of actual role data vs a list of expected role118 Validate a list of actual role data vs a list of expected role
115 data.119 data.
116 """120 """
121 self.log.debug('Validating role data...')
117 self.log.debug('actual: {}'.format(repr(actual)))122 self.log.debug('actual: {}'.format(repr(actual)))
118 for e in expected:123 for e in expected:
119 found = False124 found = False
@@ -134,6 +139,7 @@
134 Validate a list of actual user data vs a list of expected user139 Validate a list of actual user data vs a list of expected user
135 data.140 data.
136 """141 """
142 self.log.debug('Validating user data...')
137 self.log.debug('actual: {}'.format(repr(actual)))143 self.log.debug('actual: {}'.format(repr(actual)))
138 for e in expected:144 for e in expected:
139 found = False145 found = False
@@ -155,17 +161,20 @@
155161
156 Validate a list of actual flavors vs a list of expected flavors.162 Validate a list of actual flavors vs a list of expected flavors.
157 """163 """
164 self.log.debug('Validating flavor data...')
158 self.log.debug('actual: {}'.format(repr(actual)))165 self.log.debug('actual: {}'.format(repr(actual)))
159 act = [a.name for a in actual]166 act = [a.name for a in actual]
160 return self._validate_list_data(expected, act)167 return self._validate_list_data(expected, act)
161168
162 def tenant_exists(self, keystone, tenant):169 def tenant_exists(self, keystone, tenant):
163 """Return True if tenant exists."""170 """Return True if tenant exists."""
171 self.log.debug('Checking if tenant exists ({})...'.format(tenant))
164 return tenant in [t.name for t in keystone.tenants.list()]172 return tenant in [t.name for t in keystone.tenants.list()]
165173
166 def authenticate_keystone_admin(self, keystone_sentry, user, password,174 def authenticate_keystone_admin(self, keystone_sentry, user, password,
167 tenant):175 tenant):
168 """Authenticates admin user with the keystone admin endpoint."""176 """Authenticates admin user with the keystone admin endpoint."""
177 self.log.debug('Authenticating keystone admin...')
169 unit = keystone_sentry178 unit = keystone_sentry
170 service_ip = unit.relation('shared-db',179 service_ip = unit.relation('shared-db',
171 'mysql:shared-db')['private-address']180 'mysql:shared-db')['private-address']
@@ -175,6 +184,7 @@
175184
176 def authenticate_keystone_user(self, keystone, user, password, tenant):185 def authenticate_keystone_user(self, keystone, user, password, tenant):
177 """Authenticates a regular user with the keystone public endpoint."""186 """Authenticates a regular user with the keystone public endpoint."""
187 self.log.debug('Authenticating keystone user ({})...'.format(user))
178 ep = keystone.service_catalog.url_for(service_type='identity',188 ep = keystone.service_catalog.url_for(service_type='identity',
179 endpoint_type='publicURL')189 endpoint_type='publicURL')
180 return keystone_client.Client(username=user, password=password,190 return keystone_client.Client(username=user, password=password,
@@ -182,12 +192,21 @@
182192
183 def authenticate_glance_admin(self, keystone):193 def authenticate_glance_admin(self, keystone):
184 """Authenticates admin user with glance."""194 """Authenticates admin user with glance."""
195 self.log.debug('Authenticating glance admin...')
185 ep = keystone.service_catalog.url_for(service_type='image',196 ep = keystone.service_catalog.url_for(service_type='image',
186 endpoint_type='adminURL')197 endpoint_type='adminURL')
187 return glance_client.Client(ep, token=keystone.auth_token)198 return glance_client.Client(ep, token=keystone.auth_token)
188199
200 def authenticate_heat_admin(self, keystone):
201 """Authenticates the admin user with heat."""
202 self.log.debug('Authenticating heat admin...')
203 ep = keystone.service_catalog.url_for(service_type='orchestration',
204 endpoint_type='publicURL')
205 return heat_client.Client(endpoint=ep, token=keystone.auth_token)
206
189 def authenticate_nova_user(self, keystone, user, password, tenant):207 def authenticate_nova_user(self, keystone, user, password, tenant):
190 """Authenticates a regular user with nova-api."""208 """Authenticates a regular user with nova-api."""
209 self.log.debug('Authenticating nova user ({})...'.format(user))
191 ep = keystone.service_catalog.url_for(service_type='identity',210 ep = keystone.service_catalog.url_for(service_type='identity',
192 endpoint_type='publicURL')211 endpoint_type='publicURL')
193 return nova_client.Client(username=user, api_key=password,212 return nova_client.Client(username=user, api_key=password,
@@ -195,6 +214,7 @@
195214
196 def create_cirros_image(self, glance, image_name):215 def create_cirros_image(self, glance, image_name):
197 """Download the latest cirros image and upload it to glance."""216 """Download the latest cirros image and upload it to glance."""
217 self.log.debug('Creating glance image ({})...'.format(image_name))
198 http_proxy = os.getenv('AMULET_HTTP_PROXY')218 http_proxy = os.getenv('AMULET_HTTP_PROXY')
199 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))219 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
200 if http_proxy:220 if http_proxy:
@@ -235,6 +255,11 @@
235255
236 def delete_image(self, glance, image):256 def delete_image(self, glance, image):
237 """Delete the specified image."""257 """Delete the specified image."""
258
259 # /!\ DEPRECATION WARNING
260 self.log.warn('/!\\ DEPRECATION WARNING: use '
261 'delete_resource instead of delete_image.')
262 self.log.debug('Deleting glance image ({})...'.format(image))
238 num_before = len(list(glance.images.list()))263 num_before = len(list(glance.images.list()))
239 glance.images.delete(image)264 glance.images.delete(image)
240265
@@ -254,6 +279,8 @@
254279
255 def create_instance(self, nova, image_name, instance_name, flavor):280 def create_instance(self, nova, image_name, instance_name, flavor):
256 """Create the specified instance."""281 """Create the specified instance."""
282 self.log.debug('Creating instance '
283 '({}|{}|{})'.format(instance_name, image_name, flavor))
257 image = nova.images.find(name=image_name)284 image = nova.images.find(name=image_name)
258 flavor = nova.flavors.find(name=flavor)285 flavor = nova.flavors.find(name=flavor)
259 instance = nova.servers.create(name=instance_name, image=image,286 instance = nova.servers.create(name=instance_name, image=image,
@@ -276,6 +303,11 @@
276303
277 def delete_instance(self, nova, instance):304 def delete_instance(self, nova, instance):
278 """Delete the specified instance."""305 """Delete the specified instance."""
306
307 # /!\ DEPRECATION WARNING
308 self.log.warn('/!\\ DEPRECATION WARNING: use '
309 'delete_resource instead of delete_instance.')
310 self.log.debug('Deleting instance ({})...'.format(instance))
279 num_before = len(list(nova.servers.list()))311 num_before = len(list(nova.servers.list()))
280 nova.servers.delete(instance)312 nova.servers.delete(instance)
281313
@@ -292,3 +324,90 @@
292 return False324 return False
293325
294 return True326 return True
327
328 def create_or_get_keypair(self, nova, keypair_name="testkey"):
329 """Create a new keypair, or return pointer if it already exists."""
330 try:
331 _keypair = nova.keypairs.get(keypair_name)
332 self.log.debug('Keypair ({}) already exists, '
333 'using it.'.format(keypair_name))
334 return _keypair
335 except:
336 self.log.debug('Keypair ({}) does not exist, '
337 'creating it.'.format(keypair_name))
338
339 _keypair = nova.keypairs.create(name=keypair_name)
340 return _keypair
341
342 def delete_resource(self, resource, resource_id,
343 msg="resource", max_wait=120):
344 """Delete one openstack resource, such as one instance, keypair,
345 image, volume, stack, etc., and confirm deletion within max wait time.
346
347 :param resource: pointer to os resource type, ex:glance_client.images
348 :param resource_id: unique name or id for the openstack resource
349 :param msg: text to identify purpose in logging
350 :param max_wait: maximum wait time in seconds
351 :returns: True if successful, otherwise False
352 """
353 num_before = len(list(resource.list()))
354 resource.delete(resource_id)
355
356 tries = 0
357 num_after = len(list(resource.list()))
358 while num_after != (num_before - 1) and tries < (max_wait / 4):
359 self.log.debug('{} delete check: '
360 '{} [{}:{}] {}'.format(msg, tries,
361 num_before,
362 num_after,
363 resource_id))
364 time.sleep(4)
365 num_after = len(list(resource.list()))
366 tries += 1
367
368 self.log.debug('{}: expected, actual count = {}, '
369 '{}'.format(msg, num_before - 1, num_after))
370
371 if num_after == (num_before - 1):
372 return True
373 else:
374 self.log.error('{} delete timed out'.format(msg))
375 return False
376
377 def resource_reaches_status(self, resource, resource_id,
378 expected_stat='available',
379 msg='resource', max_wait=120):
380 """Wait for an openstack resources status to reach an
381 expected status within a specified time. Useful to confirm that
382 nova instances, cinder vols, snapshots, glance images, heat stacks
383 and other resources eventually reach the expected status.
384
385 :param resource: pointer to os resource type, ex: heat_client.stacks
386 :param resource_id: unique id for the openstack resource
387 :param expected_stat: status to expect resource to reach
388 :param msg: text to identify purpose in logging
389 :param max_wait: maximum wait time in seconds
390 :returns: True if successful, False if status is not reached
391 """
392
393 tries = 0
394 resource_stat = resource.get(resource_id).status
395 while resource_stat != expected_stat and tries < (max_wait / 4):
396 self.log.debug('{} status check: '
397 '{} [{}:{}] {}'.format(msg, tries,
398 resource_stat,
399 expected_stat,
400 resource_id))
401 time.sleep(4)
402 resource_stat = resource.get(resource_id).status
403 tries += 1
404
405 self.log.debug('{}: expected, actual status = {}, '
406 '{}'.format(msg, resource_stat, expected_stat))
407
408 if resource_stat == expected_stat:
409 return True
410 else:
411 self.log.debug('{} never reached expected status: '
412 '{}'.format(resource_id, expected_stat))
413 return False

Subscribers

People subscribed via source and target branches