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

Proposed by Corey Bryant
Status: Merged
Approved by: Billy Olsen
Approved revision: 120
Merged at revision: 120
Proposed branch: lp:~corey.bryant/charms/trusty/glance/fix-global-reqs
Merge into: lp:~openstack-charmers-archive/charms/trusty/glance/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/glance/fix-global-reqs
Reviewer Review Type Date Requested Status
Billy Olsen Approve
Review via email: mp+262470@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 #5473 glance-next for corey.bryant mp262470
    LINT OK: passed

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

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

charm_unit_test #5105 glance-next for corey.bryant mp262470
    UNIT OK: passed

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

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

charm_amulet_test #4680 glance-next for corey.bryant mp262470
    AMULET OK: passed

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

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
1=== modified file 'hooks/charmhelpers/contrib/hahelpers/cluster.py'
2--- hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-06-10 20:31:46 +0000
3+++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-06-19 16:15:08 +0000
4@@ -64,6 +64,10 @@
5 pass
6
7
8+class CRMDCNotFound(Exception):
9+ pass
10+
11+
12 def is_elected_leader(resource):
13 """
14 Returns True if the charm executing this is the elected cluster leader.
15@@ -116,8 +120,9 @@
16 status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
17 if not isinstance(status, six.text_type):
18 status = six.text_type(status, "utf-8")
19- except subprocess.CalledProcessError:
20- return False
21+ except subprocess.CalledProcessError as ex:
22+ raise CRMDCNotFound(str(ex))
23+
24 current_dc = ''
25 for line in status.split('\n'):
26 if line.startswith('Current DC'):
27@@ -125,10 +130,14 @@
28 current_dc = line.split(':')[1].split()[0]
29 if current_dc == get_unit_hostname():
30 return True
31+ elif current_dc == 'NONE':
32+ raise CRMDCNotFound('Current DC: NONE')
33+
34 return False
35
36
37-@retry_on_exception(5, base_delay=2, exc_type=CRMResourceNotFound)
38+@retry_on_exception(5, base_delay=2,
39+ exc_type=(CRMResourceNotFound, CRMDCNotFound))
40 def is_crm_leader(resource, retry=False):
41 """
42 Returns True if the charm calling this is the elected corosync leader,
43
44=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
45--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-04 08:44:46 +0000
46+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-19 16:15:08 +0000
47@@ -110,7 +110,8 @@
48 (self.precise_essex, self.precise_folsom, self.precise_grizzly,
49 self.precise_havana, self.precise_icehouse,
50 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
51- self.trusty_kilo, self.vivid_kilo) = range(10)
52+ self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,
53+ self.wily_liberty) = range(12)
54
55 releases = {
56 ('precise', None): self.precise_essex,
57@@ -121,8 +122,10 @@
58 ('trusty', None): self.trusty_icehouse,
59 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
60 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
61+ ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
62 ('utopic', None): self.utopic_juno,
63- ('vivid', None): self.vivid_kilo}
64+ ('vivid', None): self.vivid_kilo,
65+ ('wily', None): self.wily_liberty}
66 return releases[(self.series, self.openstack)]
67
68 def _get_openstack_release_string(self):
69@@ -138,6 +141,7 @@
70 ('trusty', 'icehouse'),
71 ('utopic', 'juno'),
72 ('vivid', 'kilo'),
73+ ('wily', 'liberty'),
74 ])
75 if self.openstack:
76 os_origin = self.openstack.split(':')[1]
77
78=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/utils.py'
79--- hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-03-20 17:15:02 +0000
80+++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-06-19 16:15:08 +0000
81@@ -16,15 +16,15 @@
82
83 import logging
84 import os
85+import six
86 import time
87 import urllib
88
89 import glanceclient.v1.client as glance_client
90+import heatclient.v1.client as heat_client
91 import keystoneclient.v2_0 as keystone_client
92 import novaclient.v1_1.client as nova_client
93
94-import six
95-
96 from charmhelpers.contrib.amulet.utils import (
97 AmuletUtils
98 )
99@@ -37,7 +37,7 @@
100 """OpenStack amulet utilities.
101
102 This class inherits from AmuletUtils and has additional support
103- that is specifically for use by OpenStack charms.
104+ that is specifically for use by OpenStack charm tests.
105 """
106
107 def __init__(self, log_level=ERROR):
108@@ -51,6 +51,8 @@
109 Validate actual endpoint data vs expected endpoint data. The ports
110 are used to find the matching endpoint.
111 """
112+ self.log.debug('Validating endpoint data...')
113+ self.log.debug('actual: {}'.format(repr(endpoints)))
114 found = False
115 for ep in endpoints:
116 self.log.debug('endpoint: {}'.format(repr(ep)))
117@@ -77,6 +79,7 @@
118 Validate a list of actual service catalog endpoints vs a list of
119 expected service catalog endpoints.
120 """
121+ self.log.debug('Validating service catalog endpoint data...')
122 self.log.debug('actual: {}'.format(repr(actual)))
123 for k, v in six.iteritems(expected):
124 if k in actual:
125@@ -93,6 +96,7 @@
126 Validate a list of actual tenant data vs list of expected tenant
127 data.
128 """
129+ self.log.debug('Validating tenant data...')
130 self.log.debug('actual: {}'.format(repr(actual)))
131 for e in expected:
132 found = False
133@@ -114,6 +118,7 @@
134 Validate a list of actual role data vs a list of expected role
135 data.
136 """
137+ self.log.debug('Validating role data...')
138 self.log.debug('actual: {}'.format(repr(actual)))
139 for e in expected:
140 found = False
141@@ -134,6 +139,7 @@
142 Validate a list of actual user data vs a list of expected user
143 data.
144 """
145+ self.log.debug('Validating user data...')
146 self.log.debug('actual: {}'.format(repr(actual)))
147 for e in expected:
148 found = False
149@@ -155,17 +161,20 @@
150
151 Validate a list of actual flavors vs a list of expected flavors.
152 """
153+ self.log.debug('Validating flavor data...')
154 self.log.debug('actual: {}'.format(repr(actual)))
155 act = [a.name for a in actual]
156 return self._validate_list_data(expected, act)
157
158 def tenant_exists(self, keystone, tenant):
159 """Return True if tenant exists."""
160+ self.log.debug('Checking if tenant exists ({})...'.format(tenant))
161 return tenant in [t.name for t in keystone.tenants.list()]
162
163 def authenticate_keystone_admin(self, keystone_sentry, user, password,
164 tenant):
165 """Authenticates admin user with the keystone admin endpoint."""
166+ self.log.debug('Authenticating keystone admin...')
167 unit = keystone_sentry
168 service_ip = unit.relation('shared-db',
169 'mysql:shared-db')['private-address']
170@@ -175,6 +184,7 @@
171
172 def authenticate_keystone_user(self, keystone, user, password, tenant):
173 """Authenticates a regular user with the keystone public endpoint."""
174+ self.log.debug('Authenticating keystone user ({})...'.format(user))
175 ep = keystone.service_catalog.url_for(service_type='identity',
176 endpoint_type='publicURL')
177 return keystone_client.Client(username=user, password=password,
178@@ -182,12 +192,21 @@
179
180 def authenticate_glance_admin(self, keystone):
181 """Authenticates admin user with glance."""
182+ self.log.debug('Authenticating glance admin...')
183 ep = keystone.service_catalog.url_for(service_type='image',
184 endpoint_type='adminURL')
185 return glance_client.Client(ep, token=keystone.auth_token)
186
187+ def authenticate_heat_admin(self, keystone):
188+ """Authenticates the admin user with heat."""
189+ self.log.debug('Authenticating heat admin...')
190+ ep = keystone.service_catalog.url_for(service_type='orchestration',
191+ endpoint_type='publicURL')
192+ return heat_client.Client(endpoint=ep, token=keystone.auth_token)
193+
194 def authenticate_nova_user(self, keystone, user, password, tenant):
195 """Authenticates a regular user with nova-api."""
196+ self.log.debug('Authenticating nova user ({})...'.format(user))
197 ep = keystone.service_catalog.url_for(service_type='identity',
198 endpoint_type='publicURL')
199 return nova_client.Client(username=user, api_key=password,
200@@ -195,6 +214,7 @@
201
202 def create_cirros_image(self, glance, image_name):
203 """Download the latest cirros image and upload it to glance."""
204+ self.log.debug('Creating glance image ({})...'.format(image_name))
205 http_proxy = os.getenv('AMULET_HTTP_PROXY')
206 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
207 if http_proxy:
208@@ -235,6 +255,11 @@
209
210 def delete_image(self, glance, image):
211 """Delete the specified image."""
212+
213+ # /!\ DEPRECATION WARNING
214+ self.log.warn('/!\\ DEPRECATION WARNING: use '
215+ 'delete_resource instead of delete_image.')
216+ self.log.debug('Deleting glance image ({})...'.format(image))
217 num_before = len(list(glance.images.list()))
218 glance.images.delete(image)
219
220@@ -254,6 +279,8 @@
221
222 def create_instance(self, nova, image_name, instance_name, flavor):
223 """Create the specified instance."""
224+ self.log.debug('Creating instance '
225+ '({}|{}|{})'.format(instance_name, image_name, flavor))
226 image = nova.images.find(name=image_name)
227 flavor = nova.flavors.find(name=flavor)
228 instance = nova.servers.create(name=instance_name, image=image,
229@@ -276,6 +303,11 @@
230
231 def delete_instance(self, nova, instance):
232 """Delete the specified instance."""
233+
234+ # /!\ DEPRECATION WARNING
235+ self.log.warn('/!\\ DEPRECATION WARNING: use '
236+ 'delete_resource instead of delete_instance.')
237+ self.log.debug('Deleting instance ({})...'.format(instance))
238 num_before = len(list(nova.servers.list()))
239 nova.servers.delete(instance)
240
241@@ -292,3 +324,90 @@
242 return False
243
244 return True
245+
246+ def create_or_get_keypair(self, nova, keypair_name="testkey"):
247+ """Create a new keypair, or return pointer if it already exists."""
248+ try:
249+ _keypair = nova.keypairs.get(keypair_name)
250+ self.log.debug('Keypair ({}) already exists, '
251+ 'using it.'.format(keypair_name))
252+ return _keypair
253+ except:
254+ self.log.debug('Keypair ({}) does not exist, '
255+ 'creating it.'.format(keypair_name))
256+
257+ _keypair = nova.keypairs.create(name=keypair_name)
258+ return _keypair
259+
260+ def delete_resource(self, resource, resource_id,
261+ msg="resource", max_wait=120):
262+ """Delete one openstack resource, such as one instance, keypair,
263+ image, volume, stack, etc., and confirm deletion within max wait time.
264+
265+ :param resource: pointer to os resource type, ex:glance_client.images
266+ :param resource_id: unique name or id for the openstack resource
267+ :param msg: text to identify purpose in logging
268+ :param max_wait: maximum wait time in seconds
269+ :returns: True if successful, otherwise False
270+ """
271+ num_before = len(list(resource.list()))
272+ resource.delete(resource_id)
273+
274+ tries = 0
275+ num_after = len(list(resource.list()))
276+ while num_after != (num_before - 1) and tries < (max_wait / 4):
277+ self.log.debug('{} delete check: '
278+ '{} [{}:{}] {}'.format(msg, tries,
279+ num_before,
280+ num_after,
281+ resource_id))
282+ time.sleep(4)
283+ num_after = len(list(resource.list()))
284+ tries += 1
285+
286+ self.log.debug('{}: expected, actual count = {}, '
287+ '{}'.format(msg, num_before - 1, num_after))
288+
289+ if num_after == (num_before - 1):
290+ return True
291+ else:
292+ self.log.error('{} delete timed out'.format(msg))
293+ return False
294+
295+ def resource_reaches_status(self, resource, resource_id,
296+ expected_stat='available',
297+ msg='resource', max_wait=120):
298+ """Wait for an openstack resources status to reach an
299+ expected status within a specified time. Useful to confirm that
300+ nova instances, cinder vols, snapshots, glance images, heat stacks
301+ and other resources eventually reach the expected status.
302+
303+ :param resource: pointer to os resource type, ex: heat_client.stacks
304+ :param resource_id: unique id for the openstack resource
305+ :param expected_stat: status to expect resource to reach
306+ :param msg: text to identify purpose in logging
307+ :param max_wait: maximum wait time in seconds
308+ :returns: True if successful, False if status is not reached
309+ """
310+
311+ tries = 0
312+ resource_stat = resource.get(resource_id).status
313+ while resource_stat != expected_stat and tries < (max_wait / 4):
314+ self.log.debug('{} status check: '
315+ '{} [{}:{}] {}'.format(msg, tries,
316+ resource_stat,
317+ expected_stat,
318+ resource_id))
319+ time.sleep(4)
320+ resource_stat = resource.get(resource_id).status
321+ tries += 1
322+
323+ self.log.debug('{}: expected, actual status = {}, '
324+ '{}'.format(msg, resource_stat, expected_stat))
325+
326+ if resource_stat == expected_stat:
327+ return True
328+ else:
329+ self.log.debug('{} never reached expected status: '
330+ '{}'.format(resource_id, expected_stat))
331+ return False
332
333=== modified file 'hooks/charmhelpers/contrib/openstack/context.py'
334--- hooks/charmhelpers/contrib/openstack/context.py 2015-05-11 07:28:22 +0000
335+++ hooks/charmhelpers/contrib/openstack/context.py 2015-06-19 16:15:08 +0000
336@@ -240,7 +240,7 @@
337 if self.relation_prefix:
338 password_setting = self.relation_prefix + '_password'
339
340- for rid in relation_ids('shared-db'):
341+ for rid in relation_ids(self.interfaces[0]):
342 for unit in related_units(rid):
343 rdata = relation_get(rid=rid, unit=unit)
344 host = rdata.get('db_host')
345
346=== modified file 'hooks/charmhelpers/contrib/openstack/neutron.py'
347--- hooks/charmhelpers/contrib/openstack/neutron.py 2015-06-10 20:31:46 +0000
348+++ hooks/charmhelpers/contrib/openstack/neutron.py 2015-06-19 16:15:08 +0000
349@@ -172,14 +172,16 @@
350 'services': ['calico-felix',
351 'bird',
352 'neutron-dhcp-agent',
353- 'nova-api-metadata'],
354+ 'nova-api-metadata',
355+ 'etcd'],
356 'packages': [[headers_package()] + determine_dkms_package(),
357 ['calico-compute',
358 'bird',
359 'neutron-dhcp-agent',
360- 'nova-api-metadata']],
361- 'server_packages': ['neutron-server', 'calico-control'],
362- 'server_services': ['neutron-server']
363+ 'nova-api-metadata',
364+ 'etcd']],
365+ 'server_packages': ['neutron-server', 'calico-control', 'etcd'],
366+ 'server_services': ['neutron-server', 'etcd']
367 },
368 'vsp': {
369 'config': '/etc/neutron/plugins/nuage/nuage_plugin.ini',
370
371=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
372--- hooks/charmhelpers/contrib/openstack/utils.py 2015-06-04 08:44:46 +0000
373+++ hooks/charmhelpers/contrib/openstack/utils.py 2015-06-19 16:15:08 +0000
374@@ -79,6 +79,7 @@
375 ('trusty', 'icehouse'),
376 ('utopic', 'juno'),
377 ('vivid', 'kilo'),
378+ ('wily', 'liberty'),
379 ])
380
381
382@@ -91,6 +92,7 @@
383 ('2014.1', 'icehouse'),
384 ('2014.2', 'juno'),
385 ('2015.1', 'kilo'),
386+ ('2015.2', 'liberty'),
387 ])
388
389 # The ugly duckling
390@@ -113,6 +115,7 @@
391 ('2.2.0', 'juno'),
392 ('2.2.1', 'kilo'),
393 ('2.2.2', 'kilo'),
394+ ('2.3.0', 'liberty'),
395 ])
396
397 DEFAULT_LOOPBACK_SIZE = '5G'
398@@ -321,6 +324,9 @@
399 'kilo': 'trusty-updates/kilo',
400 'kilo/updates': 'trusty-updates/kilo',
401 'kilo/proposed': 'trusty-proposed/kilo',
402+ 'liberty': 'trusty-updates/liberty',
403+ 'liberty/updates': 'trusty-updates/liberty',
404+ 'liberty/proposed': 'trusty-proposed/liberty',
405 }
406
407 try:
408@@ -549,6 +555,11 @@
409
410 pip_create_virtualenv(os.path.join(parent_dir, 'venv'))
411
412+ # Upgrade setuptools from default virtualenv version. The default version
413+ # in trusty breaks update.py in global requirements master branch.
414+ pip_install('setuptools', upgrade=True, proxy=http_proxy,
415+ venv=os.path.join(parent_dir, 'venv'))
416+
417 for p in projects['repositories']:
418 repo = p['repository']
419 branch = p['branch']
420@@ -610,24 +621,24 @@
421 else:
422 repo_dir = dest_dir
423
424+ venv = os.path.join(parent_dir, 'venv')
425+
426 if update_requirements:
427 if not requirements_dir:
428 error_out('requirements repo must be cloned before '
429 'updating from global requirements.')
430- _git_update_requirements(repo_dir, requirements_dir)
431+ _git_update_requirements(venv, repo_dir, requirements_dir)
432
433 juju_log('Installing git repo from dir: {}'.format(repo_dir))
434 if http_proxy:
435- pip_install(repo_dir, proxy=http_proxy,
436- venv=os.path.join(parent_dir, 'venv'))
437+ pip_install(repo_dir, proxy=http_proxy, venv=venv)
438 else:
439- pip_install(repo_dir,
440- venv=os.path.join(parent_dir, 'venv'))
441+ pip_install(repo_dir, venv=venv)
442
443 return repo_dir
444
445
446-def _git_update_requirements(package_dir, reqs_dir):
447+def _git_update_requirements(venv, package_dir, reqs_dir):
448 """
449 Update from global requirements.
450
451@@ -636,12 +647,14 @@
452 """
453 orig_dir = os.getcwd()
454 os.chdir(reqs_dir)
455- cmd = ['python', 'update.py', package_dir]
456+ python = os.path.join(venv, 'bin/python')
457+ cmd = [python, 'update.py', package_dir]
458 try:
459 subprocess.check_call(cmd)
460 except subprocess.CalledProcessError:
461 package = os.path.basename(package_dir)
462- error_out("Error updating {} from global-requirements.txt".format(package))
463+ error_out("Error updating {} from "
464+ "global-requirements.txt".format(package))
465 os.chdir(orig_dir)
466
467
468
469=== modified file 'hooks/charmhelpers/core/host.py'
470--- hooks/charmhelpers/core/host.py 2015-06-10 20:31:46 +0000
471+++ hooks/charmhelpers/core/host.py 2015-06-19 16:15:08 +0000
472@@ -24,6 +24,7 @@
473 import os
474 import re
475 import pwd
476+import glob
477 import grp
478 import random
479 import string
480@@ -269,6 +270,21 @@
481 return None
482
483
484+def path_hash(path):
485+ """
486+ Generate a hash checksum of all files matching 'path'. Standard wildcards
487+ like '*' and '?' are supported, see documentation for the 'glob' module for
488+ more information.
489+
490+ :return: dict: A { filename: hash } dictionary for all matched files.
491+ Empty if none found.
492+ """
493+ return {
494+ filename: file_hash(filename)
495+ for filename in glob.iglob(path)
496+ }
497+
498+
499 def check_hash(path, checksum, hash_type='md5'):
500 """
501 Validate a file using a cryptographic checksum.
502@@ -296,23 +312,25 @@
503
504 @restart_on_change({
505 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
506+ '/etc/apache/sites-enabled/*': [ 'apache2' ]
507 })
508- def ceph_client_changed():
509+ def config_changed():
510 pass # your code here
511
512 In this example, the cinder-api and cinder-volume services
513 would be restarted if /etc/ceph/ceph.conf is changed by the
514- ceph_client_changed function.
515+ ceph_client_changed function. The apache2 service would be
516+ restarted if any file matching the pattern got changed, created
517+ or removed. Standard wildcards are supported, see documentation
518+ for the 'glob' module for more information.
519 """
520 def wrap(f):
521 def wrapped_f(*args, **kwargs):
522- checksums = {}
523- for path in restart_map:
524- checksums[path] = file_hash(path)
525+ checksums = {path: path_hash(path) for path in restart_map}
526 f(*args, **kwargs)
527 restarts = []
528 for path in restart_map:
529- if checksums[path] != file_hash(path):
530+ if path_hash(path) != checksums[path]:
531 restarts += restart_map[path]
532 services_list = list(OrderedDict.fromkeys(restarts))
533 if not stopstart:
534
535=== modified file 'tests/charmhelpers/contrib/amulet/utils.py'
536--- tests/charmhelpers/contrib/amulet/utils.py 2015-05-05 20:24:43 +0000
537+++ tests/charmhelpers/contrib/amulet/utils.py 2015-06-19 16:15:08 +0000
538@@ -15,13 +15,15 @@
539 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
540
541 import ConfigParser
542+import distro_info
543 import io
544 import logging
545+import os
546 import re
547+import six
548 import sys
549 import time
550-
551-import six
552+import urlparse
553
554
555 class AmuletUtils(object):
556@@ -33,6 +35,7 @@
557
558 def __init__(self, log_level=logging.ERROR):
559 self.log = self.get_logger(level=log_level)
560+ self.ubuntu_releases = self.get_ubuntu_releases()
561
562 def get_logger(self, name="amulet-logger", level=logging.DEBUG):
563 """Get a logger object that will log to stdout."""
564@@ -70,12 +73,44 @@
565 else:
566 return False
567
568+ def get_ubuntu_release_from_sentry(self, sentry_unit):
569+ """Get Ubuntu release codename from sentry unit.
570+
571+ :param sentry_unit: amulet sentry/service unit pointer
572+ :returns: list of strings - release codename, failure message
573+ """
574+ msg = None
575+ cmd = 'lsb_release -cs'
576+ release, code = sentry_unit.run(cmd)
577+ if code == 0:
578+ self.log.debug('{} lsb_release: {}'.format(
579+ sentry_unit.info['unit_name'], release))
580+ else:
581+ msg = ('{} `{}` returned {} '
582+ '{}'.format(sentry_unit.info['unit_name'],
583+ cmd, release, code))
584+ if release not in self.ubuntu_releases:
585+ msg = ("Release ({}) not found in Ubuntu releases "
586+ "({})".format(release, self.ubuntu_releases))
587+ return release, msg
588+
589 def validate_services(self, commands):
590- """Validate services.
591-
592- Verify the specified services are running on the corresponding
593+ """Validate that lists of commands succeed on service units. Can be
594+ used to verify system services are running on the corresponding
595 service units.
596- """
597+
598+ :param commands: dict with sentry keys and arbitrary command list vals
599+ :returns: None if successful, Failure string message otherwise
600+ """
601+ self.log.debug('Checking status of system services...')
602+
603+ # /!\ DEPRECATION WARNING (beisner):
604+ # New and existing tests should be rewritten to use
605+ # validate_services_by_name() as it is aware of init systems.
606+ self.log.warn('/!\\ DEPRECATION WARNING: use '
607+ 'validate_services_by_name instead of validate_services '
608+ 'due to init system differences.')
609+
610 for k, v in six.iteritems(commands):
611 for cmd in v:
612 output, code = k.run(cmd)
613@@ -86,6 +121,41 @@
614 return "command `{}` returned {}".format(cmd, str(code))
615 return None
616
617+ def validate_services_by_name(self, sentry_services):
618+ """Validate system service status by service name, automatically
619+ detecting init system based on Ubuntu release codename.
620+
621+ :param sentry_services: dict with sentry keys and svc list values
622+ :returns: None if successful, Failure string message otherwise
623+ """
624+ self.log.debug('Checking status of system services...')
625+
626+ # Point at which systemd became a thing
627+ systemd_switch = self.ubuntu_releases.index('vivid')
628+
629+ for sentry_unit, services_list in six.iteritems(sentry_services):
630+ # Get lsb_release codename from unit
631+ release, ret = self.get_ubuntu_release_from_sentry(sentry_unit)
632+ if ret:
633+ return ret
634+
635+ for service_name in services_list:
636+ if (self.ubuntu_releases.index(release) >= systemd_switch or
637+ service_name == "rabbitmq-server"):
638+ # init is systemd
639+ cmd = 'sudo service {} status'.format(service_name)
640+ elif self.ubuntu_releases.index(release) < systemd_switch:
641+ # init is upstart
642+ cmd = 'sudo status {}'.format(service_name)
643+
644+ output, code = sentry_unit.run(cmd)
645+ self.log.debug('{} `{}` returned '
646+ '{}'.format(sentry_unit.info['unit_name'],
647+ cmd, code))
648+ if code != 0:
649+ return "command `{}` returned {}".format(cmd, str(code))
650+ return None
651+
652 def _get_config(self, unit, filename):
653 """Get a ConfigParser object for parsing a unit's config file."""
654 file_contents = unit.file_contents(filename)
655@@ -104,6 +174,9 @@
656 Verify that the specified section of the config file contains
657 the expected option key:value pairs.
658 """
659+ self.log.debug('Validating config file data ({} in {} on {})'
660+ '...'.format(section, config_file,
661+ sentry_unit.info['unit_name']))
662 config = self._get_config(sentry_unit, config_file)
663
664 if section != 'DEFAULT' and not config.has_section(section):
665@@ -321,3 +394,15 @@
666
667 def endpoint_error(self, name, data):
668 return 'unexpected endpoint data in {} - {}'.format(name, data)
669+
670+ def get_ubuntu_releases(self):
671+ """Return a list of all Ubuntu releases in order of release."""
672+ _d = distro_info.UbuntuDistroInfo()
673+ _release_list = _d.all
674+ self.log.debug('Ubuntu release list: {}'.format(_release_list))
675+ return _release_list
676+
677+ def file_to_url(self, file_rel_path):
678+ """Convert a relative file path to a file URL."""
679+ _abs_path = os.path.abspath(file_rel_path)
680+ return urlparse.urlparse(_abs_path, scheme='file').geturl()
681
682=== modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
683--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-04 08:44:46 +0000
684+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-19 16:15:08 +0000
685@@ -110,7 +110,8 @@
686 (self.precise_essex, self.precise_folsom, self.precise_grizzly,
687 self.precise_havana, self.precise_icehouse,
688 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
689- self.trusty_kilo, self.vivid_kilo) = range(10)
690+ self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,
691+ self.wily_liberty) = range(12)
692
693 releases = {
694 ('precise', None): self.precise_essex,
695@@ -121,8 +122,10 @@
696 ('trusty', None): self.trusty_icehouse,
697 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
698 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
699+ ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
700 ('utopic', None): self.utopic_juno,
701- ('vivid', None): self.vivid_kilo}
702+ ('vivid', None): self.vivid_kilo,
703+ ('wily', None): self.wily_liberty}
704 return releases[(self.series, self.openstack)]
705
706 def _get_openstack_release_string(self):
707@@ -138,6 +141,7 @@
708 ('trusty', 'icehouse'),
709 ('utopic', 'juno'),
710 ('vivid', 'kilo'),
711+ ('wily', 'liberty'),
712 ])
713 if self.openstack:
714 os_origin = self.openstack.split(':')[1]
715
716=== modified file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
717--- tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-03-20 17:15:02 +0000
718+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-06-19 16:15:08 +0000
719@@ -16,15 +16,15 @@
720
721 import logging
722 import os
723+import six
724 import time
725 import urllib
726
727 import glanceclient.v1.client as glance_client
728+import heatclient.v1.client as heat_client
729 import keystoneclient.v2_0 as keystone_client
730 import novaclient.v1_1.client as nova_client
731
732-import six
733-
734 from charmhelpers.contrib.amulet.utils import (
735 AmuletUtils
736 )
737@@ -37,7 +37,7 @@
738 """OpenStack amulet utilities.
739
740 This class inherits from AmuletUtils and has additional support
741- that is specifically for use by OpenStack charms.
742+ that is specifically for use by OpenStack charm tests.
743 """
744
745 def __init__(self, log_level=ERROR):
746@@ -51,6 +51,8 @@
747 Validate actual endpoint data vs expected endpoint data. The ports
748 are used to find the matching endpoint.
749 """
750+ self.log.debug('Validating endpoint data...')
751+ self.log.debug('actual: {}'.format(repr(endpoints)))
752 found = False
753 for ep in endpoints:
754 self.log.debug('endpoint: {}'.format(repr(ep)))
755@@ -77,6 +79,7 @@
756 Validate a list of actual service catalog endpoints vs a list of
757 expected service catalog endpoints.
758 """
759+ self.log.debug('Validating service catalog endpoint data...')
760 self.log.debug('actual: {}'.format(repr(actual)))
761 for k, v in six.iteritems(expected):
762 if k in actual:
763@@ -93,6 +96,7 @@
764 Validate a list of actual tenant data vs list of expected tenant
765 data.
766 """
767+ self.log.debug('Validating tenant data...')
768 self.log.debug('actual: {}'.format(repr(actual)))
769 for e in expected:
770 found = False
771@@ -114,6 +118,7 @@
772 Validate a list of actual role data vs a list of expected role
773 data.
774 """
775+ self.log.debug('Validating role data...')
776 self.log.debug('actual: {}'.format(repr(actual)))
777 for e in expected:
778 found = False
779@@ -134,6 +139,7 @@
780 Validate a list of actual user data vs a list of expected user
781 data.
782 """
783+ self.log.debug('Validating user data...')
784 self.log.debug('actual: {}'.format(repr(actual)))
785 for e in expected:
786 found = False
787@@ -155,17 +161,20 @@
788
789 Validate a list of actual flavors vs a list of expected flavors.
790 """
791+ self.log.debug('Validating flavor data...')
792 self.log.debug('actual: {}'.format(repr(actual)))
793 act = [a.name for a in actual]
794 return self._validate_list_data(expected, act)
795
796 def tenant_exists(self, keystone, tenant):
797 """Return True if tenant exists."""
798+ self.log.debug('Checking if tenant exists ({})...'.format(tenant))
799 return tenant in [t.name for t in keystone.tenants.list()]
800
801 def authenticate_keystone_admin(self, keystone_sentry, user, password,
802 tenant):
803 """Authenticates admin user with the keystone admin endpoint."""
804+ self.log.debug('Authenticating keystone admin...')
805 unit = keystone_sentry
806 service_ip = unit.relation('shared-db',
807 'mysql:shared-db')['private-address']
808@@ -175,6 +184,7 @@
809
810 def authenticate_keystone_user(self, keystone, user, password, tenant):
811 """Authenticates a regular user with the keystone public endpoint."""
812+ self.log.debug('Authenticating keystone user ({})...'.format(user))
813 ep = keystone.service_catalog.url_for(service_type='identity',
814 endpoint_type='publicURL')
815 return keystone_client.Client(username=user, password=password,
816@@ -182,12 +192,21 @@
817
818 def authenticate_glance_admin(self, keystone):
819 """Authenticates admin user with glance."""
820+ self.log.debug('Authenticating glance admin...')
821 ep = keystone.service_catalog.url_for(service_type='image',
822 endpoint_type='adminURL')
823 return glance_client.Client(ep, token=keystone.auth_token)
824
825+ def authenticate_heat_admin(self, keystone):
826+ """Authenticates the admin user with heat."""
827+ self.log.debug('Authenticating heat admin...')
828+ ep = keystone.service_catalog.url_for(service_type='orchestration',
829+ endpoint_type='publicURL')
830+ return heat_client.Client(endpoint=ep, token=keystone.auth_token)
831+
832 def authenticate_nova_user(self, keystone, user, password, tenant):
833 """Authenticates a regular user with nova-api."""
834+ self.log.debug('Authenticating nova user ({})...'.format(user))
835 ep = keystone.service_catalog.url_for(service_type='identity',
836 endpoint_type='publicURL')
837 return nova_client.Client(username=user, api_key=password,
838@@ -195,6 +214,7 @@
839
840 def create_cirros_image(self, glance, image_name):
841 """Download the latest cirros image and upload it to glance."""
842+ self.log.debug('Creating glance image ({})...'.format(image_name))
843 http_proxy = os.getenv('AMULET_HTTP_PROXY')
844 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
845 if http_proxy:
846@@ -235,6 +255,11 @@
847
848 def delete_image(self, glance, image):
849 """Delete the specified image."""
850+
851+ # /!\ DEPRECATION WARNING
852+ self.log.warn('/!\\ DEPRECATION WARNING: use '
853+ 'delete_resource instead of delete_image.')
854+ self.log.debug('Deleting glance image ({})...'.format(image))
855 num_before = len(list(glance.images.list()))
856 glance.images.delete(image)
857
858@@ -254,6 +279,8 @@
859
860 def create_instance(self, nova, image_name, instance_name, flavor):
861 """Create the specified instance."""
862+ self.log.debug('Creating instance '
863+ '({}|{}|{})'.format(instance_name, image_name, flavor))
864 image = nova.images.find(name=image_name)
865 flavor = nova.flavors.find(name=flavor)
866 instance = nova.servers.create(name=instance_name, image=image,
867@@ -276,6 +303,11 @@
868
869 def delete_instance(self, nova, instance):
870 """Delete the specified instance."""
871+
872+ # /!\ DEPRECATION WARNING
873+ self.log.warn('/!\\ DEPRECATION WARNING: use '
874+ 'delete_resource instead of delete_instance.')
875+ self.log.debug('Deleting instance ({})...'.format(instance))
876 num_before = len(list(nova.servers.list()))
877 nova.servers.delete(instance)
878
879@@ -292,3 +324,90 @@
880 return False
881
882 return True
883+
884+ def create_or_get_keypair(self, nova, keypair_name="testkey"):
885+ """Create a new keypair, or return pointer if it already exists."""
886+ try:
887+ _keypair = nova.keypairs.get(keypair_name)
888+ self.log.debug('Keypair ({}) already exists, '
889+ 'using it.'.format(keypair_name))
890+ return _keypair
891+ except:
892+ self.log.debug('Keypair ({}) does not exist, '
893+ 'creating it.'.format(keypair_name))
894+
895+ _keypair = nova.keypairs.create(name=keypair_name)
896+ return _keypair
897+
898+ def delete_resource(self, resource, resource_id,
899+ msg="resource", max_wait=120):
900+ """Delete one openstack resource, such as one instance, keypair,
901+ image, volume, stack, etc., and confirm deletion within max wait time.
902+
903+ :param resource: pointer to os resource type, ex:glance_client.images
904+ :param resource_id: unique name or id for the openstack resource
905+ :param msg: text to identify purpose in logging
906+ :param max_wait: maximum wait time in seconds
907+ :returns: True if successful, otherwise False
908+ """
909+ num_before = len(list(resource.list()))
910+ resource.delete(resource_id)
911+
912+ tries = 0
913+ num_after = len(list(resource.list()))
914+ while num_after != (num_before - 1) and tries < (max_wait / 4):
915+ self.log.debug('{} delete check: '
916+ '{} [{}:{}] {}'.format(msg, tries,
917+ num_before,
918+ num_after,
919+ resource_id))
920+ time.sleep(4)
921+ num_after = len(list(resource.list()))
922+ tries += 1
923+
924+ self.log.debug('{}: expected, actual count = {}, '
925+ '{}'.format(msg, num_before - 1, num_after))
926+
927+ if num_after == (num_before - 1):
928+ return True
929+ else:
930+ self.log.error('{} delete timed out'.format(msg))
931+ return False
932+
933+ def resource_reaches_status(self, resource, resource_id,
934+ expected_stat='available',
935+ msg='resource', max_wait=120):
936+ """Wait for an openstack resources status to reach an
937+ expected status within a specified time. Useful to confirm that
938+ nova instances, cinder vols, snapshots, glance images, heat stacks
939+ and other resources eventually reach the expected status.
940+
941+ :param resource: pointer to os resource type, ex: heat_client.stacks
942+ :param resource_id: unique id for the openstack resource
943+ :param expected_stat: status to expect resource to reach
944+ :param msg: text to identify purpose in logging
945+ :param max_wait: maximum wait time in seconds
946+ :returns: True if successful, False if status is not reached
947+ """
948+
949+ tries = 0
950+ resource_stat = resource.get(resource_id).status
951+ while resource_stat != expected_stat and tries < (max_wait / 4):
952+ self.log.debug('{} status check: '
953+ '{} [{}:{}] {}'.format(msg, tries,
954+ resource_stat,
955+ expected_stat,
956+ resource_id))
957+ time.sleep(4)
958+ resource_stat = resource.get(resource_id).status
959+ tries += 1
960+
961+ self.log.debug('{}: expected, actual status = {}, '
962+ '{}'.format(msg, resource_stat, expected_stat))
963+
964+ if resource_stat == expected_stat:
965+ return True
966+ else:
967+ self.log.debug('{} never reached expected status: '
968+ '{}'.format(resource_id, expected_stat))
969+ return False

Subscribers

People subscribed via source and target branches