Merge lp:~corey.bryant/charms/trusty/keystone/amulet-basic into lp:~openstack-charmers-archive/charms/trusty/keystone/trunk

Proposed by Corey Bryant
Status: Superseded
Proposed branch: lp:~corey.bryant/charms/trusty/keystone/amulet-basic
Merge into: lp:~openstack-charmers-archive/charms/trusty/keystone/trunk
Diff against target: 1250 lines (+999/-14)
20 files modified
Makefile (+16/-3)
charm-helpers.yaml (+4/-0)
hooks/charmhelpers/contrib/openstack/utils.py (+8/-2)
hooks/charmhelpers/core/fstab.py (+116/-0)
hooks/charmhelpers/core/host.py (+21/-7)
hooks/charmhelpers/fetch/__init__.py (+10/-1)
hooks/charmhelpers/fetch/bzrurl.py (+2/-1)
tests/00-setup (+8/-0)
tests/10-basic-precise-essex (+9/-0)
tests/11-basic-precise-folsom (+10/-0)
tests/12-basic-precise-grizzly (+10/-0)
tests/13-basic-precise-havana (+10/-0)
tests/14-basic-precise-icehouse (+10/-0)
tests/15-basic-trusty-icehouse (+9/-0)
tests/README (+28/-0)
tests/basic_deployment.py (+328/-0)
tests/charmhelpers/contrib/amulet/deployment.py (+58/-0)
tests/charmhelpers/contrib/amulet/utils.py (+153/-0)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+38/-0)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+151/-0)
To merge this branch: bzr merge lp:~corey.bryant/charms/trusty/keystone/amulet-basic
Reviewer Review Type Date Requested Status
James Page Needs Fixing
Review via email: mp+220255@code.launchpad.net

This proposal has been superseded by a proposal from 2014-06-20.

To post a comment you must log in.
Revision history for this message
James Page (james-page) wrote :

Hey Corey

Looks like a good start - here's some general and specific feedback.

1) testing supported openstack versions

I think that when I ran 'make amulet-test' amulet decided to choose precise, and hence the essex openstack packages; this entire test is valid for essex, folsom, grizzly, havana and icehouse on precise and icehouse on trusty; I suggest refactoring into a class which can be called with various different configurations to represent the series that the charm supports deploying - something like:

   ks = KeyStoneDeployment(series="precise", openstack="folsom")
   ks.runtests()

that's rough - but gives you an idea of what I was thinking about! You could then have 10-basic-precise-essex, 10-basic-precise-folsom etc....

regression testing ftw!

2) service status

utils.run_command(mysql_unit, 'service mysql status')

Does this actually validate that mysql is running? I'm pretty sure status always returns 0 via the service wrapper - calling "status mysql" might do something better.

3) inspecting relationships

In addition to testing that services are running and that keystone is usable, it would be nice to ensure that the expected relation data has been set between keystone <-> mysql.

3) adding an additional service

To exercise all relations, you might want to add something like cinder into the mix to ensure that the catalog contains both keystone and cinder endpoints post deployment.

4) logging

sys.stderr.write feels bad - how about using python logging instead? that gives timestamps etc.. and can write to stderr.

64. By James Page

[tribaal,r=james-page,t=james-page]

Resync helpers to pickup fixes for apt lock races and better block device detection and handling.

65. By James Page

[trivial] Ensure lint and test pass before publish

Revision history for this message
Corey Bryant (corey.bryant) wrote :

Hey James,

Thanks for reviewing. I've pushed a new version of the code, with your comments accounted for. I still need to do some cleanup but I think it's in good enough shape for another review. In terms of cleanup, I have a bunch of notes scattered in comments to do some regex comparisons for relation and endpoint data to get more accurate checking. Also I can make some of this code more reusable (for example, TestDeployment class could inherit from another class that at least defines run()).

Corey

Revision history for this message
James Page (james-page) wrote :

Corey

Looks like a good step forward - like the re-use for series of Ubuntu and OpenStack.

Re the testing of services, endpoints and relation state - might be worth trying to work that into dictionaries of data which you could just then write a generic validator for rather than having lots of if .... : for each check.

That could work back into charm-helpers as part of amulet test supporting stuff.

Also the __init__ function looks quite big; it might be better to just construct the class with init and (maybe create the Deployment object) and then have extra functions for deploying services and configuring. That way some of that code could be shared as well.

Looking good tho!

Revision history for this message
James Page (james-page) wrote :

Hi Corey

Really like the rework - its looking good. I made a few specific inline comments re superclass calls.

Its probably also worth calling the AmuletDeployment class OpenStackAmuletDeployment - it appears todo alot of OpenStack specific things so lets not confuse the default namespace for the time being.

review: Needs Fixing
Revision history for this message
Corey Bryant (corey.bryant) wrote :

Hi James,

Thanks for the review. I might just keep AmuletDeployment as a more generic class and create OpenStackAmulet(AmuletDeployment) that includes the openstack specific differences.

Off I go to rework.

Corey

Revision history for this message
Corey Bryant (corey.bryant) wrote :

Hi James,

I've pushed this again with fixes for your most recent comments plus the following:
 *I added logging support in amulet_utils.py
 *I updated the config option used in test_restart_on_config_change()

python-amulet isn't available yet though so I don't think this can be merged until that's available.

Corey

66. By James Page

resync helpers for juno support

67. By James Page

[mikemc,r=james-page] Add product-streams service catalog type.

Revision history for this message
Corey Bryant (corey.bryant) wrote :

I've pushed a new version with the following updates:
 *Makefile - Use standard target names test and unit_test per https://docs.google.com/a/canonical.com/document/d/1Lb86vjXG7ZnVqDq8idNa5-rx_VB728Qacxm4U7IutgE/edit#
 *amulet_utils.py - Change get_logger() defaults.
 *amulet_utils.py - Added validate_flavor_data() and _validate_list_data() (used by nova-cloud-controller charm)
 *amulet_utils.py - Added -f option to pgrep in _get_proc_start_time() to match against full command line (not just process name) and only use first pid found
 *amulet_utils.py - Added more code to validate_endpoint_data(), making calls to this function simpler
 *basic_deployment.py - Simplified code in test_keystone_endpoint() and test_cinder_endpoint()
 *basic_deployment.py - test_keystone_shared_db_relation() - changed expected 'auth_host' value to utils.valid_ip
 *basic_deployment.py - Changed test_restart_on_config_change() to check for restart of keystone-all service instead of keystone

Revision history for this message
Corey Bryant (corey.bryant) wrote :

I've pushed a new version with the following updates:
  *amulet_utils.py - Changed pgrep to return only the oldest matching
                     pid in _get_proc_start_time()
  *amulet_utils.py - Changed sleep(5) to sleep(10) in
                     service_restarted()

Revision history for this message
Corey Bryant (corey.bryant) wrote :

I've pushed a new version with the following updates:
  *tests/README - Added README file
  *tests/amulet_deployment.py - Added support to _add_services() to allows
                                number of service units to be specified
  *tests/amulet_utils.py - Added tenant_exists(), authenticate_keystone_admin(),
                           and authenticate_keystone_user()
  *tests/basic_deployment.py - Added support to _add_services() to specify
                               number of service units
  *tests/basic_deployment.py - Reduce _initialize_tests() code by using
                               authenticate_keystone_admin() and
                               authenticate_keystone_user()
  *tests/basic_deployment.py - Make _initialize_tests() idempotent by not
                               adding demo tenant/role/user if they already
                               exist

Revision history for this message
Corey Bryant (corey.bryant) wrote :

I've pushed a new version with the following updates:
  * tests/00-setup - install python-amulet as it just became available via packaging

Revision history for this message
Corey Bryant (corey.bryant) wrote :

I've pushed a new version now that I've moved all common code to charm-helpers.

68. By Corey Bryant

Move charm-helpers.yaml to charm-helpers-hooks.yaml and
add charm-helpers-tests.yaml.

69. By Corey Bryant

Sync with charm-helpers

70. By Corey Bryant

Add initial Amulet basic tests

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2014-05-21 10:13:58 +0000
3+++ Makefile 2014-06-20 13:39:25 +0000
4@@ -1,13 +1,26 @@
5 #!/usr/bin/make
6 PYTHON := /usr/bin/env python
7
8+prereq:
9+ @[ -z "`dpkg -l | grep python-flake8 -q`" ] && \
10+ sudo apt-get install --yes python-flake8
11+ @[ -z "`dpkg -l | grep charm-tools -q`" ] && \
12+ sudo apt-get install --yes charm-tools
13+
14 lint:
15- @flake8 --exclude hooks/charmhelpers hooks unit_tests
16+ @flake8 --exclude hooks/charmhelpers hooks unit_tests tests
17 @charm proof
18
19+unit_test:
20+ @echo Starting unit tests...
21+ @$(PYTHON) /usr/bin/nosetests --nologcapture --with-coverage unit_tests
22+
23 test:
24- @echo Starting tests...
25- @$(PYTHON) /usr/bin/nosetests --nologcapture --with-coverage unit_tests
26+ @echo Starting Amulet tests...
27+ # coreycb note: The -v should only be temporary until Amulet sends
28+ # raise_status() messages to stderr:
29+ # https://bugs.launchpad.net/amulet/+bug/1320357
30+ @juju test -v
31
32 sync:
33 @charm-helper-sync -c charm-helpers.yaml
34
35=== modified file 'charm-helpers.yaml'
36--- charm-helpers.yaml 2014-03-28 10:39:49 +0000
37+++ charm-helpers.yaml 2014-06-20 13:39:25 +0000
38@@ -11,3 +11,7 @@
39 - contrib.unison
40 - payload.execd
41 - contrib.peerstorage
42+destination: tests/charmhelpers
43+include:
44+ - contrib.amulet
45+ - contrib.openstack.amulet
46
47=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
48--- hooks/charmhelpers/contrib/openstack/utils.py 2014-05-19 11:42:30 +0000
49+++ hooks/charmhelpers/contrib/openstack/utils.py 2014-06-20 13:39:25 +0000
50@@ -3,7 +3,6 @@
51 # Common python helper functions used for OpenStack charms.
52 from collections import OrderedDict
53
54-import apt_pkg as apt
55 import subprocess
56 import os
57 import socket
58@@ -41,7 +40,8 @@
59 ('quantal', 'folsom'),
60 ('raring', 'grizzly'),
61 ('saucy', 'havana'),
62- ('trusty', 'icehouse')
63+ ('trusty', 'icehouse'),
64+ ('utopic', 'juno'),
65 ])
66
67
68@@ -52,6 +52,7 @@
69 ('2013.1', 'grizzly'),
70 ('2013.2', 'havana'),
71 ('2014.1', 'icehouse'),
72+ ('2014.2', 'juno'),
73 ])
74
75 # The ugly duckling
76@@ -130,6 +131,7 @@
77
78 def get_os_codename_package(package, fatal=True):
79 '''Derive OpenStack release codename from an installed package.'''
80+ import apt_pkg as apt
81 apt.init()
82
83 # Tell apt to build an in-memory cache to prevent race conditions (if
84@@ -273,6 +275,9 @@
85 'icehouse': 'precise-updates/icehouse',
86 'icehouse/updates': 'precise-updates/icehouse',
87 'icehouse/proposed': 'precise-proposed/icehouse',
88+ 'juno': 'trusty-updates/juno',
89+ 'juno/updates': 'trusty-updates/juno',
90+ 'juno/proposed': 'trusty-proposed/juno',
91 }
92
93 try:
94@@ -320,6 +325,7 @@
95
96 """
97
98+ import apt_pkg as apt
99 src = config('openstack-origin')
100 cur_vers = get_os_version_package(package)
101 available_vers = get_os_version_install_source(src)
102
103=== added file 'hooks/charmhelpers/core/fstab.py'
104--- hooks/charmhelpers/core/fstab.py 1970-01-01 00:00:00 +0000
105+++ hooks/charmhelpers/core/fstab.py 2014-06-20 13:39:25 +0000
106@@ -0,0 +1,116 @@
107+#!/usr/bin/env python
108+# -*- coding: utf-8 -*-
109+
110+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
111+
112+import os
113+
114+
115+class Fstab(file):
116+ """This class extends file in order to implement a file reader/writer
117+ for file `/etc/fstab`
118+ """
119+
120+ class Entry(object):
121+ """Entry class represents a non-comment line on the `/etc/fstab` file
122+ """
123+ def __init__(self, device, mountpoint, filesystem,
124+ options, d=0, p=0):
125+ self.device = device
126+ self.mountpoint = mountpoint
127+ self.filesystem = filesystem
128+
129+ if not options:
130+ options = "defaults"
131+
132+ self.options = options
133+ self.d = d
134+ self.p = p
135+
136+ def __eq__(self, o):
137+ return str(self) == str(o)
138+
139+ def __str__(self):
140+ return "{} {} {} {} {} {}".format(self.device,
141+ self.mountpoint,
142+ self.filesystem,
143+ self.options,
144+ self.d,
145+ self.p)
146+
147+ DEFAULT_PATH = os.path.join(os.path.sep, 'etc', 'fstab')
148+
149+ def __init__(self, path=None):
150+ if path:
151+ self._path = path
152+ else:
153+ self._path = self.DEFAULT_PATH
154+ file.__init__(self, self._path, 'r+')
155+
156+ def _hydrate_entry(self, line):
157+ # NOTE: use split with no arguments to split on any
158+ # whitespace including tabs
159+ return Fstab.Entry(*filter(
160+ lambda x: x not in ('', None),
161+ line.strip("\n").split()))
162+
163+ @property
164+ def entries(self):
165+ self.seek(0)
166+ for line in self.readlines():
167+ try:
168+ if not line.startswith("#"):
169+ yield self._hydrate_entry(line)
170+ except ValueError:
171+ pass
172+
173+ def get_entry_by_attr(self, attr, value):
174+ for entry in self.entries:
175+ e_attr = getattr(entry, attr)
176+ if e_attr == value:
177+ return entry
178+ return None
179+
180+ def add_entry(self, entry):
181+ if self.get_entry_by_attr('device', entry.device):
182+ return False
183+
184+ self.write(str(entry) + '\n')
185+ self.truncate()
186+ return entry
187+
188+ def remove_entry(self, entry):
189+ self.seek(0)
190+
191+ lines = self.readlines()
192+
193+ found = False
194+ for index, line in enumerate(lines):
195+ if not line.startswith("#"):
196+ if self._hydrate_entry(line) == entry:
197+ found = True
198+ break
199+
200+ if not found:
201+ return False
202+
203+ lines.remove(line)
204+
205+ self.seek(0)
206+ self.write(''.join(lines))
207+ self.truncate()
208+ return True
209+
210+ @classmethod
211+ def remove_by_mountpoint(cls, mountpoint, path=None):
212+ fstab = cls(path=path)
213+ entry = fstab.get_entry_by_attr('mountpoint', mountpoint)
214+ if entry:
215+ return fstab.remove_entry(entry)
216+ return False
217+
218+ @classmethod
219+ def add(cls, device, mountpoint, filesystem, options=None, path=None):
220+ return cls(path=path).add_entry(Fstab.Entry(device,
221+ mountpoint, filesystem,
222+ options=options))
223
224=== modified file 'hooks/charmhelpers/core/host.py'
225--- hooks/charmhelpers/core/host.py 2014-05-19 11:42:30 +0000
226+++ hooks/charmhelpers/core/host.py 2014-06-20 13:39:25 +0000
227@@ -12,11 +12,11 @@
228 import string
229 import subprocess
230 import hashlib
231-import apt_pkg
232
233 from collections import OrderedDict
234
235 from hookenv import log
236+from fstab import Fstab
237
238
239 def service_start(service_name):
240@@ -35,7 +35,8 @@
241
242
243 def service_reload(service_name, restart_on_failure=False):
244- """Reload a system service, optionally falling back to restart if reload fails"""
245+ """Reload a system service, optionally falling back to restart if
246+ reload fails"""
247 service_result = service('reload', service_name)
248 if not service_result and restart_on_failure:
249 service_result = service('restart', service_name)
250@@ -144,7 +145,19 @@
251 target.write(content)
252
253
254-def mount(device, mountpoint, options=None, persist=False):
255+def fstab_remove(mp):
256+ """Remove the given mountpoint entry from /etc/fstab
257+ """
258+ return Fstab.remove_by_mountpoint(mp)
259+
260+
261+def fstab_add(dev, mp, fs, options=None):
262+ """Adds the given device entry to the /etc/fstab file
263+ """
264+ return Fstab.add(dev, mp, fs, options=options)
265+
266+
267+def mount(device, mountpoint, options=None, persist=False, filesystem="ext3"):
268 """Mount a filesystem at a particular mountpoint"""
269 cmd_args = ['mount']
270 if options is not None:
271@@ -155,9 +168,9 @@
272 except subprocess.CalledProcessError, e:
273 log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
274 return False
275+
276 if persist:
277- # TODO: update fstab
278- pass
279+ return fstab_add(device, mountpoint, filesystem, options=options)
280 return True
281
282
283@@ -169,9 +182,9 @@
284 except subprocess.CalledProcessError, e:
285 log('Error unmounting {}\n{}'.format(mountpoint, e.output))
286 return False
287+
288 if persist:
289- # TODO: update fstab
290- pass
291+ return fstab_remove(mountpoint)
292 return True
293
294
295@@ -304,6 +317,7 @@
296 0 => Installed revno is the same as supplied arg
297 -1 => Installed revno is less than supplied arg
298 '''
299+ import apt_pkg
300 if not pkgcache:
301 apt_pkg.init()
302 pkgcache = apt_pkg.Cache()
303
304=== modified file 'hooks/charmhelpers/fetch/__init__.py'
305--- hooks/charmhelpers/fetch/__init__.py 2014-05-19 11:42:30 +0000
306+++ hooks/charmhelpers/fetch/__init__.py 2014-06-20 13:39:25 +0000
307@@ -13,7 +13,6 @@
308 config,
309 log,
310 )
311-import apt_pkg
312 import os
313
314
315@@ -56,6 +55,15 @@
316 'icehouse/proposed': 'precise-proposed/icehouse',
317 'precise-icehouse/proposed': 'precise-proposed/icehouse',
318 'precise-proposed/icehouse': 'precise-proposed/icehouse',
319+ # Juno
320+ 'juno': 'trusty-updates/juno',
321+ 'trusty-juno': 'trusty-updates/juno',
322+ 'trusty-juno/updates': 'trusty-updates/juno',
323+ 'trusty-updates/juno': 'trusty-updates/juno',
324+ 'juno/proposed': 'trusty-proposed/juno',
325+ 'juno/proposed': 'trusty-proposed/juno',
326+ 'trusty-juno/proposed': 'trusty-proposed/juno',
327+ 'trusty-proposed/juno': 'trusty-proposed/juno',
328 }
329
330 # The order of this list is very important. Handlers should be listed in from
331@@ -108,6 +116,7 @@
332
333 def filter_installed_packages(packages):
334 """Returns a list of packages that require installation"""
335+ import apt_pkg
336 apt_pkg.init()
337
338 # Tell apt to build an in-memory cache to prevent race conditions (if
339
340=== modified file 'hooks/charmhelpers/fetch/bzrurl.py'
341--- hooks/charmhelpers/fetch/bzrurl.py 2014-03-27 10:54:38 +0000
342+++ hooks/charmhelpers/fetch/bzrurl.py 2014-06-20 13:39:25 +0000
343@@ -39,7 +39,8 @@
344 def install(self, source):
345 url_parts = self.parse_url(source)
346 branch_name = url_parts.path.strip("/").split("/")[-1]
347- dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched", branch_name)
348+ dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
349+ branch_name)
350 if not os.path.exists(dest_dir):
351 mkdir(dest_dir, perms=0755)
352 try:
353
354=== added directory 'tests'
355=== added file 'tests/00-setup'
356--- tests/00-setup 1970-01-01 00:00:00 +0000
357+++ tests/00-setup 2014-06-20 13:39:25 +0000
358@@ -0,0 +1,8 @@
359+#!/bin/bash
360+
361+set -ex
362+
363+sudo add-apt-repository --yes ppa:juju/stable
364+sudo apt-get update --yes
365+sudo apt-get install --yes python-amulet
366+sudo apt-get install --yes python-keystoneclient
367
368=== added file 'tests/10-basic-precise-essex'
369--- tests/10-basic-precise-essex 1970-01-01 00:00:00 +0000
370+++ tests/10-basic-precise-essex 2014-06-20 13:39:25 +0000
371@@ -0,0 +1,9 @@
372+#!/usr/bin/python
373+
374+"""Amulet tests on a basic keystone deployment on precise-essex."""
375+
376+from basic_deployment import KeystoneBasicDeployment
377+
378+if __name__ == '__main__':
379+ deployment = KeystoneBasicDeployment(series='precise')
380+ deployment.run_tests()
381
382=== added file 'tests/11-basic-precise-folsom'
383--- tests/11-basic-precise-folsom 1970-01-01 00:00:00 +0000
384+++ tests/11-basic-precise-folsom 2014-06-20 13:39:25 +0000
385@@ -0,0 +1,10 @@
386+#!/usr/bin/python
387+
388+"""Amulet tests on a basic keystone deployment on precise-folsom."""
389+
390+from basic_deployment import KeystoneBasicDeployment
391+
392+if __name__ == '__main__':
393+ deployment = KeystoneBasicDeployment(series='precise',
394+ openstack='cloud:precise-folsom')
395+ deployment.run_tests()
396
397=== added file 'tests/12-basic-precise-grizzly'
398--- tests/12-basic-precise-grizzly 1970-01-01 00:00:00 +0000
399+++ tests/12-basic-precise-grizzly 2014-06-20 13:39:25 +0000
400@@ -0,0 +1,10 @@
401+#!/usr/bin/python
402+
403+"""Amulet tests on a basic keystone deployment on precise-grizzly."""
404+
405+from basic_deployment import KeystoneBasicDeployment
406+
407+if __name__ == '__main__':
408+ deployment = KeystoneBasicDeployment(series='precise',
409+ openstack='cloud:precise-grizzly')
410+ deployment.run_tests()
411
412=== added file 'tests/13-basic-precise-havana'
413--- tests/13-basic-precise-havana 1970-01-01 00:00:00 +0000
414+++ tests/13-basic-precise-havana 2014-06-20 13:39:25 +0000
415@@ -0,0 +1,10 @@
416+#!/usr/bin/python
417+
418+"""Amulet tests on a basic keystone deployment on precise-havana."""
419+
420+from basic_deployment import KeystoneBasicDeployment
421+
422+if __name__ == '__main__':
423+ deployment = KeystoneBasicDeployment(series='precise',
424+ openstack='cloud:precise-havana')
425+ deployment.run_tests()
426
427=== added file 'tests/14-basic-precise-icehouse'
428--- tests/14-basic-precise-icehouse 1970-01-01 00:00:00 +0000
429+++ tests/14-basic-precise-icehouse 2014-06-20 13:39:25 +0000
430@@ -0,0 +1,10 @@
431+#!/usr/bin/python
432+
433+"""Amulet tests on a basic keystone deployment on precise-icehouse."""
434+
435+from basic_deployment import KeystoneBasicDeployment
436+
437+if __name__ == '__main__':
438+ deployment = KeystoneBasicDeployment(series='precise',
439+ openstack='cloud:precise-icehouse')
440+ deployment.run_tests()
441
442=== added file 'tests/15-basic-trusty-icehouse'
443--- tests/15-basic-trusty-icehouse 1970-01-01 00:00:00 +0000
444+++ tests/15-basic-trusty-icehouse 2014-06-20 13:39:25 +0000
445@@ -0,0 +1,9 @@
446+#!/usr/bin/python
447+
448+"""Amulet tests on a basic keystone deployment on trusty-icehouse."""
449+
450+from basic_deployment import KeystoneBasicDeployment
451+
452+if __name__ == '__main__':
453+ deployment = KeystoneBasicDeployment(series='trusty')
454+ deployment.run_tests()
455
456=== added file 'tests/README'
457--- tests/README 1970-01-01 00:00:00 +0000
458+++ tests/README 2014-06-20 13:39:25 +0000
459@@ -0,0 +1,28 @@
460+This directory provides Amulet tests that focus on verification of Keystone
461+deployments.
462+
463+The following examples demonstrate different ways that tests can be executed.
464+All examples are run from the charm's root directory.
465+
466+ * To run all tests (starting with 00-setup):
467+
468+ make test
469+
470+ * To run a specific test module (or modules):
471+
472+ juju test -v 15-basic-trusty-icehouse
473+
474+ * To run a specific test module (or modules), and keep the environment
475+ deployed after a failure:
476+
477+ juju test --set-e -v 15-basic-trusty-icehouse
478+
479+ * To re-run a test module against an already deployed environment (one
480+ that was deployed by a previous call to 'juju test --set-e'):
481+
482+ ./tests/15-basic-trusty-icehouse
483+
484+For debugging and test development purposes, all code should be idempotent.
485+In other words, the code should have the ability to be re-run without changing
486+the results beyond the initial run. This enables editing and re-running of a
487+test module against an already deployed environment, as described above.
488
489=== added file 'tests/basic_deployment.py'
490--- tests/basic_deployment.py 1970-01-01 00:00:00 +0000
491+++ tests/basic_deployment.py 2014-06-20 13:39:25 +0000
492@@ -0,0 +1,328 @@
493+#!/usr/bin/python
494+
495+import amulet
496+
497+from charmhelpers.contrib.openstack.amulet.deployment import (
498+ OpenStackAmuletDeployment
499+)
500+
501+from charmhelpers.contrib.openstack.amulet.utils import (
502+ OpenStackAmuletUtils,
503+ DEBUG, # flake8: noqa
504+ ERROR
505+)
506+
507+# Use DEBUG to turn on debug logging
508+u = OpenStackAmuletUtils(ERROR)
509+
510+
511+class KeystoneBasicDeployment(OpenStackAmuletDeployment):
512+ """Amulet tests on a basic keystone deployment."""
513+
514+ def __init__(self, series=None, openstack=None):
515+ """Deploy the entire test environment."""
516+ super(KeystoneBasicDeployment, self).__init__(series, openstack)
517+ self._add_services()
518+ self._add_relations()
519+ self._configure_services()
520+ self._deploy()
521+ self._initialize_tests()
522+
523+ def _add_services(self):
524+ """Add the services that we're testing, including the number of units,
525+ where keystone is local, and mysql and cinder are from the charm
526+ store."""
527+ this_service = ('keystone', 1)
528+ other_services = [('mysql', 1), ('cinder', 1)]
529+ super(KeystoneBasicDeployment, self)._add_services(this_service,
530+ other_services)
531+
532+ def _add_relations(self):
533+ """Add all of the relations for the services."""
534+ relations = {'keystone:shared-db': 'mysql:shared-db',
535+ 'cinder:identity-service': 'keystone:identity-service'}
536+ super(KeystoneBasicDeployment, self)._add_relations(relations)
537+
538+ def _configure_services(self):
539+ """Configure all of the services."""
540+ keystone_config = {'admin-password': 'openstack',
541+ 'admin-token': 'ubuntutesting'}
542+ mysql_config = {'dataset-size': '50%'}
543+ cinder_config = {'block-device': 'None'}
544+ configs = {'keystone': keystone_config,
545+ 'mysql': mysql_config,
546+ 'cinder': cinder_config}
547+ super(KeystoneBasicDeployment, self)._configure_services(configs)
548+
549+ def _initialize_tests(self):
550+ """Perform final initialization before tests get run."""
551+ # Access the sentries for inspecting service units
552+ self.mysql_sentry = self.d.sentry.unit['mysql/0']
553+ self.keystone_sentry = self.d.sentry.unit['keystone/0']
554+ self.cinder_sentry = self.d.sentry.unit['cinder/0']
555+
556+ # Authenticate admin with keystone
557+ self.keystone = u.authenticate_keystone_admin(self.keystone_sentry,
558+ user='admin',
559+ password='openstack',
560+ tenant='admin')
561+
562+ # Create a demo tenant/role/user
563+ self.demo_tenant = 'demoTenant'
564+ self.demo_role = 'demoRole'
565+ self.demo_user = 'demoUser'
566+ if not u.tenant_exists(self.keystone, self.demo_tenant):
567+ tenant = self.keystone.tenants.create(tenant_name=self.demo_tenant,
568+ description='demo tenant',
569+ enabled=True)
570+ self.keystone.roles.create(name=self.demo_role)
571+ self.keystone.users.create(name=self.demo_user, password='password',
572+ tenant_id=tenant.id,
573+ email='demo@demo.com')
574+
575+ # Authenticate demo user with keystone
576+ self.keystone_demo = u.authenticate_keystone_user(self.keystone,
577+ user=self.demo_user,
578+ password='password',
579+ tenant=self.demo_tenant)
580+
581+ def test_services(self):
582+ """Verify the expected services are running on the corresponding
583+ service units."""
584+ commands = {
585+ self.mysql_sentry: 'status mysql',
586+ self.keystone_sentry: 'status keystone',
587+ self.cinder_sentry: 'status cinder-api',
588+ self.cinder_sentry: 'status cinder-scheduler',
589+ self.cinder_sentry: 'status cinder-volume'
590+ }
591+ ret = u.validate_services(commands)
592+ if ret:
593+ amulet.raise_status(amulet.FAIL, msg=ret)
594+
595+ def test_tenants(self):
596+ """Verify all existing tenants."""
597+ tenant1 = {'enabled': True,
598+ 'description': 'Created by Juju',
599+ 'name': 'services',
600+ 'id': u.not_null}
601+ tenant2 = {'enabled': True,
602+ 'description': 'demo tenant',
603+ 'name': 'demoTenant',
604+ 'id': u.not_null}
605+ tenant3 = {'enabled': True,
606+ 'description': 'Created by Juju',
607+ 'name': 'admin',
608+ 'id': u.not_null}
609+ expected = [tenant1, tenant2, tenant3]
610+ actual = self.keystone.tenants.list()
611+
612+ ret = u.validate_tenant_data(expected, actual)
613+ if ret:
614+ amulet.raise_status(amulet.FAIL, msg=ret)
615+
616+ def test_roles(self):
617+ """Verify all existing roles."""
618+ role1 = {'name': 'demoRole', 'id': u.not_null}
619+ role2 = {'name': 'KeystoneAdmin', 'id': u.not_null}
620+ role3 = {'name': 'KeystoneServiceAdmin', 'id': u.not_null}
621+ role4 = {'name': 'Admin', 'id': u.not_null}
622+ expected = [role1, role2, role3, role4]
623+ actual = self.keystone.roles.list()
624+
625+ ret = u.validate_role_data(expected, actual)
626+ if ret:
627+ amulet.raise_status(amulet.FAIL, msg=ret)
628+
629+ def test_users(self):
630+ """Verify all existing roles."""
631+ user1 = {'name': 'demoUser',
632+ 'enabled': True,
633+ 'tenantId': u.not_null,
634+ 'id': u.not_null,
635+ 'email': 'demo@demo.com'}
636+ user2 = {'name': 'admin',
637+ 'enabled': True,
638+ 'tenantId': u.not_null,
639+ 'id': u.not_null,
640+ 'email': 'juju@localhost'}
641+ user3 = {'name': 'cinder',
642+ 'enabled': True,
643+ 'tenantId': u.not_null,
644+ 'id': u.not_null,
645+ 'email': u'juju@localhost'}
646+ expected = [user1, user2, user3]
647+ actual = self.keystone.users.list()
648+
649+ ret = u.validate_user_data(expected, actual)
650+ if ret:
651+ amulet.raise_status(amulet.FAIL, msg=ret)
652+
653+ def test_service_catalog(self):
654+ """Verify that the service catalog endpoint data is valid."""
655+ endpoint_vol = {'adminURL': u.valid_url,
656+ 'region': 'RegionOne',
657+ 'publicURL': u.valid_url,
658+ 'internalURL': u.valid_url}
659+ endpoint_id = {'adminURL': u.valid_url,
660+ 'region': 'RegionOne',
661+ 'publicURL': u.valid_url,
662+ 'internalURL': u.valid_url}
663+ if self._get_openstack_release() > self.precise_essex:
664+ endpoint_vol['id'] = u.not_null
665+ endpoint_id['id'] = u.not_null
666+ expected = {'volume': [endpoint_vol], 'identity': [endpoint_id]}
667+ actual = self.keystone_demo.service_catalog.get_endpoints()
668+
669+ ret = u.validate_svc_catalog_endpoint_data(expected, actual)
670+ if ret:
671+ amulet.raise_status(amulet.FAIL, msg=ret)
672+
673+ def test_keystone_endpoint(self):
674+ """Verify the keystone endpoint data."""
675+ endpoints = self.keystone.endpoints.list()
676+ admin_port = '35357'
677+ internal_port = public_port = '5000'
678+ expected = {'id': u.not_null,
679+ 'region': 'RegionOne',
680+ 'adminurl': u.valid_url,
681+ 'internalurl': u.valid_url,
682+ 'publicurl': u.valid_url,
683+ 'service_id': u.not_null}
684+ ret = u.validate_endpoint_data(endpoints, admin_port, internal_port,
685+ public_port, expected)
686+ if ret:
687+ amulet.raise_status(amulet.FAIL,
688+ msg='keystone endpoint: {}'.format(ret))
689+
690+ def test_cinder_endpoint(self):
691+ """Verify the cinder endpoint data."""
692+ endpoints = self.keystone.endpoints.list()
693+ admin_port = internal_port = public_port = '8776'
694+ expected = {'id': u.not_null,
695+ 'region': 'RegionOne',
696+ 'adminurl': u.valid_url,
697+ 'internalurl': u.valid_url,
698+ 'publicurl': u.valid_url,
699+ 'service_id': u.not_null}
700+ ret = u.validate_endpoint_data(endpoints, admin_port, internal_port,
701+ public_port, expected)
702+ if ret:
703+ amulet.raise_status(amulet.FAIL,
704+ msg='cinder endpoint: {}'.format(ret))
705+
706+ def test_keystone_shared_db_relation(self):
707+ """Verify the keystone shared-db relation data"""
708+ unit = self.keystone_sentry
709+ relation = ['shared-db', 'mysql:shared-db']
710+ expected = {
711+ 'username': 'keystone',
712+ 'private-address': u.valid_ip,
713+ 'hostname': u.valid_ip,
714+ 'database': 'keystone'
715+ }
716+ ret = u.validate_relation_data(unit, relation, expected)
717+ if ret:
718+ message = u.relation_error('keystone shared-db', ret)
719+ amulet.raise_status(amulet.FAIL, msg=message)
720+
721+ def test_mysql_shared_db_relation(self):
722+ """Verify the mysql shared-db relation data"""
723+ unit = self.mysql_sentry
724+ relation = ['shared-db', 'keystone:shared-db']
725+ expected_data = {
726+ 'private-address': u.valid_ip,
727+ 'password': u.not_null,
728+ 'db_host': u.valid_ip
729+ }
730+ ret = u.validate_relation_data(unit, relation, expected_data)
731+ if ret:
732+ message = u.relation_error('mysql shared-db', ret)
733+ amulet.raise_status(amulet.FAIL, msg=message)
734+
735+ def test_keystone_identity_service_relation(self):
736+ """Verify the keystone identity-service relation data"""
737+ unit = self.keystone_sentry
738+ relation = ['identity-service', 'cinder:identity-service']
739+ expected = {
740+ 'service_protocol': 'http',
741+ 'service_tenant': 'services',
742+ 'admin_token': 'ubuntutesting',
743+ 'service_password': u.not_null,
744+ 'service_port': '5000',
745+ 'auth_port': '35357',
746+ 'auth_protocol': 'http',
747+ 'private-address': u.valid_ip,
748+ 'https_keystone': 'False',
749+ 'auth_host': u.valid_ip,
750+ 'service_username': 'cinder',
751+ 'service_tenant_id': u.not_null,
752+ 'service_host': u.valid_ip
753+ }
754+ ret = u.validate_relation_data(unit, relation, expected)
755+ if ret:
756+ message = u.relation_error('cinder identity-service', ret)
757+ amulet.raise_status(amulet.FAIL, msg=message)
758+
759+ def test_cinder_identity_service_relation(self):
760+ """Verify the cinder identity-service relation data"""
761+ unit = self.cinder_sentry
762+ relation = ['identity-service', 'keystone:identity-service']
763+ expected = {
764+ 'service': 'cinder',
765+ 'region': 'RegionOne',
766+ 'public_url': u.valid_url,
767+ 'internal_url': u.valid_url,
768+ 'private-address': u.valid_ip,
769+ 'admin_url': u.valid_url
770+ }
771+ ret = u.validate_relation_data(unit, relation, expected)
772+ if ret:
773+ message = u.relation_error('cinder identity-service', ret)
774+ amulet.raise_status(amulet.FAIL, msg=message)
775+
776+ def test_restart_on_config_change(self):
777+ """Verify that keystone is restarted when the config is changed."""
778+ self.d.configure('keystone', {'verbose': 'True'})
779+ if not u.service_restarted(self.keystone_sentry, 'keystone-all',
780+ '/etc/keystone/keystone.conf'):
781+ message = "keystone service didn't restart after config change"
782+ amulet.raise_status(amulet.FAIL, msg=message)
783+ self.d.configure('keystone', {'verbose': 'False'})
784+
785+ def test_default_config(self):
786+ """Verify the data in the keystone config file's default section,
787+ comparing some of the variables vs relation data."""
788+ unit = self.keystone_sentry
789+ conf = '/etc/keystone/keystone.conf'
790+ relation = unit.relation('identity-service', 'cinder:identity-service')
791+ expected = {'admin_token': relation['admin_token'],
792+ 'admin_port': relation['auth_port'],
793+ 'public_port': relation['service_port'],
794+ 'use_syslog': 'False',
795+ 'log_config': '/etc/keystone/logging.conf',
796+ 'debug': 'False',
797+ 'verbose': 'False'}
798+
799+ ret = u.validate_config_data(unit, conf, 'DEFAULT', expected)
800+ if ret:
801+ message = "keystone config error: {}".format(ret)
802+ amulet.raise_status(amulet.FAIL, msg=message)
803+
804+ def test_database_config(self):
805+ """Verify the data in the keystone config file's database (or sql
806+ depending on release) section, comparing vs relation data."""
807+ unit = self.keystone_sentry
808+ conf = '/etc/keystone/keystone.conf'
809+ relation = self.mysql_sentry.relation('shared-db', 'keystone:shared-db')
810+ db_uri = "mysql://{}:{}@{}/{}".format('keystone', relation['password'],
811+ relation['db_host'], 'keystone')
812+ expected = {'connection': db_uri, 'idle_timeout': '200'}
813+
814+ if self._get_openstack_release() > self.precise_havana:
815+ ret = u.validate_config_data(unit, conf, 'database', expected)
816+ else:
817+ ret = u.validate_config_data(unit, conf, 'sql', expected)
818+ if ret:
819+ message = "keystone config error: {}".format(ret)
820+ amulet.raise_status(amulet.FAIL, msg=message)
821
822=== added directory 'tests/charmhelpers'
823=== added file 'tests/charmhelpers/__init__.py'
824=== added directory 'tests/charmhelpers/contrib'
825=== added file 'tests/charmhelpers/contrib/__init__.py'
826=== added directory 'tests/charmhelpers/contrib/amulet'
827=== added file 'tests/charmhelpers/contrib/amulet/__init__.py'
828=== added file 'tests/charmhelpers/contrib/amulet/deployment.py'
829--- tests/charmhelpers/contrib/amulet/deployment.py 1970-01-01 00:00:00 +0000
830+++ tests/charmhelpers/contrib/amulet/deployment.py 2014-06-20 13:39:25 +0000
831@@ -0,0 +1,58 @@
832+import amulet
833+
834+
835+class AmuletDeployment(object):
836+ """This class provides generic Amulet deployment and test runner
837+ methods."""
838+
839+ def __init__(self, series=None):
840+ """Initialize the deployment environment."""
841+ self.series = None
842+
843+ if series:
844+ self.series = series
845+ self.d = amulet.Deployment(series=self.series)
846+ else:
847+ self.d = amulet.Deployment()
848+
849+ def _add_services(self, this_service, other_services):
850+ """Add services to the deployment where this_service is the local charm
851+ that we're focused on testing and other_services are the other
852+ charms that come from the charm store."""
853+ name, units = range(2)
854+ self.this_service = this_service[name]
855+ self.d.add(this_service[name], units=this_service[units])
856+
857+ for svc in other_services:
858+ if self.series:
859+ self.d.add(svc[name],
860+ charm='cs:{}/{}'.format(self.series, svc[name]),
861+ units=svc[units])
862+ else:
863+ self.d.add(svc[name], units=svc[units])
864+
865+ def _add_relations(self, relations):
866+ """Add all of the relations for the services."""
867+ for k, v in relations.iteritems():
868+ self.d.relate(k, v)
869+
870+ def _configure_services(self, configs):
871+ """Configure all of the services."""
872+ for service, config in configs.iteritems():
873+ self.d.configure(service, config)
874+
875+ def _deploy(self):
876+ """Deploy environment and wait for all hooks to finish executing."""
877+ try:
878+ self.d.setup()
879+ self.d.sentry.wait()
880+ except amulet.helpers.TimeoutError:
881+ amulet.raise_status(amulet.FAIL, msg="Deployment timed out")
882+ except:
883+ raise
884+
885+ def run_tests(self):
886+ """Run all of the methods that are prefixed with 'test_'."""
887+ for test in dir(self):
888+ if test.startswith('test_'):
889+ getattr(self, test)()
890
891=== added file 'tests/charmhelpers/contrib/amulet/utils.py'
892--- tests/charmhelpers/contrib/amulet/utils.py 1970-01-01 00:00:00 +0000
893+++ tests/charmhelpers/contrib/amulet/utils.py 2014-06-20 13:39:25 +0000
894@@ -0,0 +1,153 @@
895+import ConfigParser
896+import io
897+import logging
898+import re
899+import sys
900+from time import sleep
901+
902+
903+class AmuletUtils(object):
904+ """This class provides common utility functions that are used by Amulet
905+ tests."""
906+
907+ def __init__(self, log_level=logging.ERROR):
908+ self.log = self.get_logger(level=log_level)
909+
910+ def get_logger(self, name="amulet-logger", level=logging.DEBUG):
911+ """Get a logger object that will log to stdout."""
912+ log = logging
913+ logger = log.getLogger(name)
914+ fmt = \
915+ log.Formatter("%(asctime)s %(funcName)s %(levelname)s: %(message)s")
916+
917+ handler = log.StreamHandler(stream=sys.stdout)
918+ handler.setLevel(level)
919+ handler.setFormatter(fmt)
920+
921+ logger.addHandler(handler)
922+ logger.setLevel(level)
923+
924+ return logger
925+
926+ def valid_ip(self, ip):
927+ if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip):
928+ return True
929+ else:
930+ return False
931+
932+ def valid_url(self, url):
933+ p = re.compile(
934+ r'^(?:http|ftp)s?://'
935+ r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # flake8: noqa
936+ r'localhost|'
937+ r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
938+ r'(?::\d+)?'
939+ r'(?:/?|[/?]\S+)$',
940+ re.IGNORECASE)
941+ if p.match(url):
942+ return True
943+ else:
944+ return False
945+
946+ def validate_services(self, commands):
947+ """Verify the specified services are running on the corresponding
948+ service units."""
949+ for k, v in commands.iteritems():
950+ output, code = k.run(v)
951+ if code != 0:
952+ return "command `{}` returned {}".format(v, str(code))
953+ return None
954+
955+ def _get_config(self, unit, filename):
956+ """Get a ConfigParser object for parsing a unit's config file."""
957+ file_contents = unit.file_contents(filename)
958+ config = ConfigParser.ConfigParser()
959+ config.readfp(io.StringIO(file_contents))
960+ return config
961+
962+ def validate_config_data(self, sentry_unit, config_file, section, expected):
963+ """Verify that the specified section of the config file contains
964+ the expected option key:value pairs."""
965+ config = self._get_config(sentry_unit, config_file)
966+
967+ if section != 'DEFAULT' and not config.has_section(section):
968+ return "section [{}] does not exist".format(section)
969+
970+ for k in expected.keys():
971+ if not config.has_option(section, k):
972+ return "section [{}] is missing option {}".format(section, k)
973+ if config.get(section, k) != expected[k]:
974+ return "section [{}] {}:{} != expected {}:{}".format(section,
975+ k, config.get(section, k), k, expected[k])
976+ return None
977+
978+ def _validate_dict_data(self, expected, actual):
979+ """Compare expected dictionary data vs actual dictionary data.
980+ The values in the 'expected' dictionary can be strings, bools, ints,
981+ longs, or can be a function that evaluate a variable and returns a
982+ bool."""
983+ for k, v in expected.iteritems():
984+ if k in actual:
985+ if isinstance(v, basestring) or \
986+ isinstance(v, bool) or \
987+ isinstance(v, (int, long)):
988+ if v != actual[k]:
989+ return "{}:{}".format(k, actual[k])
990+ elif not v(actual[k]):
991+ return "{}:{}".format(k, actual[k])
992+ else:
993+ return "key '{}' does not exist".format(k)
994+ return None
995+
996+ def validate_relation_data(self, sentry_unit, relation, expected):
997+ """Validate actual relation data based on expected relation data."""
998+ actual = sentry_unit.relation(relation[0], relation[1])
999+ self.log.debug('actual: {}'.format(repr(actual)))
1000+ return self._validate_dict_data(expected, actual)
1001+
1002+ def _validate_list_data(self, expected, actual):
1003+ """Compare expected list vs actual list data."""
1004+ for e in expected:
1005+ if e not in actual:
1006+ return "expected item {} not found in actual list".format(e)
1007+ return None
1008+
1009+ def not_null(self, string):
1010+ if string != None:
1011+ return True
1012+ else:
1013+ return False
1014+
1015+ def _get_file_mtime(self, sentry_unit, filename):
1016+ """Get last modification time of file."""
1017+ return sentry_unit.file_stat(filename)['mtime']
1018+
1019+ def _get_dir_mtime(self, sentry_unit, directory):
1020+ """Get last modification time of directory."""
1021+ return sentry_unit.directory_stat(directory)['mtime']
1022+
1023+ def _get_proc_start_time(self, sentry_unit, service):
1024+ """Determine start time of the process based on the last modification
1025+ time of the /proc/pid directory. The servie string will be matched
1026+ against any substring in the full command line, choosing the oldest
1027+ process."""
1028+ cmd = 'pgrep -f -o {}'.format(service)
1029+ proc_dir = '/proc/{}'.format(sentry_unit.run(cmd)[0].strip())
1030+ return self._get_dir_mtime(sentry_unit, proc_dir)
1031+
1032+ def service_restarted(self, sentry_unit, service, filename):
1033+ """Compare a service's start time vs a file's last modification time
1034+ (such as a config file for that service) to determine if the service
1035+ has been restarted."""
1036+ sleep(10)
1037+ if self._get_proc_start_time(sentry_unit, service) >= \
1038+ self._get_file_mtime(sentry_unit, filename):
1039+ return True
1040+ else:
1041+ return False
1042+
1043+ def relation_error(self, name, data):
1044+ return 'unexpected relation data in {} - {}'.format(name, data)
1045+
1046+ def endpoint_error(self, name, data):
1047+ return 'unexpected endpoint data in {} - {}'.format(name, data)
1048
1049=== added directory 'tests/charmhelpers/contrib/openstack'
1050=== added file 'tests/charmhelpers/contrib/openstack/__init__.py'
1051=== added directory 'tests/charmhelpers/contrib/openstack/amulet'
1052=== added file 'tests/charmhelpers/contrib/openstack/amulet/__init__.py'
1053=== added file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
1054--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 1970-01-01 00:00:00 +0000
1055+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2014-06-20 13:39:25 +0000
1056@@ -0,0 +1,38 @@
1057+from charmhelpers.contrib.amulet.deployment import (
1058+ AmuletDeployment
1059+)
1060+
1061+
1062+class OpenStackAmuletDeployment(AmuletDeployment):
1063+ """This class inherits from AmuletDeployment and has additional support
1064+ that is specifically for use by OpenStack charms."""
1065+
1066+ def __init__(self, series=None, openstack=None):
1067+ """Initialize the deployment environment."""
1068+ self.openstack = None
1069+ super(OpenStackAmuletDeployment, self).__init__(series)
1070+
1071+ if openstack:
1072+ self.openstack = openstack
1073+
1074+ def _configure_services(self, configs):
1075+ """Configure all of the services."""
1076+ for service, config in configs.iteritems():
1077+ if service == self.this_service:
1078+ config['openstack-origin'] = self.openstack
1079+ self.d.configure(service, config)
1080+
1081+ def _get_openstack_release(self):
1082+ """Return an integer representing the enum value of the openstack
1083+ release."""
1084+ self.precise_essex, self.precise_folsom, self.precise_grizzly, \
1085+ self.precise_havana, self.precise_icehouse, \
1086+ self.trusty_icehouse = range(6)
1087+ releases = {
1088+ ('precise', None): self.precise_essex,
1089+ ('precise', 'cloud:precise-folsom'): self.precise_folsom,
1090+ ('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
1091+ ('precise', 'cloud:precise-havana'): self.precise_havana,
1092+ ('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
1093+ ('trusty', None): self.trusty_icehouse}
1094+ return releases[(self.series, self.openstack)]
1095
1096=== added file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
1097--- tests/charmhelpers/contrib/openstack/amulet/utils.py 1970-01-01 00:00:00 +0000
1098+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2014-06-20 13:39:25 +0000
1099@@ -0,0 +1,151 @@
1100+import logging
1101+
1102+import glanceclient.v1.client as glance_client
1103+import keystoneclient.v2_0 as keystone_client
1104+import novaclient.v1_1.client as nova_client
1105+
1106+from charmhelpers.contrib.amulet.utils import (
1107+ AmuletUtils
1108+)
1109+
1110+DEBUG = logging.DEBUG
1111+ERROR = logging.ERROR
1112+
1113+
1114+class OpenStackAmuletUtils(AmuletUtils):
1115+ """This class inherits from AmuletUtils and has additional support
1116+ that is specifically for use by OpenStack charms."""
1117+
1118+ def __init__(self, log_level=ERROR):
1119+ """Initialize the deployment environment."""
1120+ super(OpenStackAmuletUtils, self).__init__(log_level)
1121+
1122+ def validate_endpoint_data(self, endpoints, admin_port, internal_port,
1123+ public_port, expected):
1124+ """Validate actual endpoint data vs expected endpoint data. The ports
1125+ are used to find the matching endpoint."""
1126+ found = False
1127+ for ep in endpoints:
1128+ self.log.debug('endpoint: {}'.format(repr(ep)))
1129+ if admin_port in ep.adminurl and internal_port in ep.internalurl \
1130+ and public_port in ep.publicurl:
1131+ found = True
1132+ actual = {'id': ep.id,
1133+ 'region': ep.region,
1134+ 'adminurl': ep.adminurl,
1135+ 'internalurl': ep.internalurl,
1136+ 'publicurl': ep.publicurl,
1137+ 'service_id': ep.service_id}
1138+ ret = self._validate_dict_data(expected, actual)
1139+ if ret:
1140+ return 'unexpected endpoint data - {}'.format(ret)
1141+
1142+ if not found:
1143+ return 'endpoint not found'
1144+
1145+ def validate_svc_catalog_endpoint_data(self, expected, actual):
1146+ """Validate a list of actual service catalog endpoints vs a list of
1147+ expected service catalog endpoints."""
1148+ self.log.debug('actual: {}'.format(repr(actual)))
1149+ for k, v in expected.iteritems():
1150+ if k in actual:
1151+ ret = self._validate_dict_data(expected[k][0], actual[k][0])
1152+ if ret:
1153+ return self.endpoint_error(k, ret)
1154+ else:
1155+ return "endpoint {} does not exist".format(k)
1156+ return ret
1157+
1158+ def validate_tenant_data(self, expected, actual):
1159+ """Validate a list of actual tenant data vs list of expected tenant
1160+ data."""
1161+ self.log.debug('actual: {}'.format(repr(actual)))
1162+ for e in expected:
1163+ found = False
1164+ for act in actual:
1165+ a = {'enabled': act.enabled, 'description': act.description,
1166+ 'name': act.name, 'id': act.id}
1167+ if e['name'] == a['name']:
1168+ found = True
1169+ ret = self._validate_dict_data(e, a)
1170+ if ret:
1171+ return "unexpected tenant data - {}".format(ret)
1172+ if not found:
1173+ return "tenant {} does not exist".format(e.name)
1174+ return ret
1175+
1176+ def validate_role_data(self, expected, actual):
1177+ """Validate a list of actual role data vs a list of expected role
1178+ data."""
1179+ self.log.debug('actual: {}'.format(repr(actual)))
1180+ for e in expected:
1181+ found = False
1182+ for act in actual:
1183+ a = {'name': act.name, 'id': act.id}
1184+ if e['name'] == a['name']:
1185+ found = True
1186+ ret = self._validate_dict_data(e, a)
1187+ if ret:
1188+ return "unexpected role data - {}".format(ret)
1189+ if not found:
1190+ return "role {} does not exist".format(e.name)
1191+ return ret
1192+
1193+ def validate_user_data(self, expected, actual):
1194+ """Validate a list of actual user data vs a list of expected user
1195+ data."""
1196+ self.log.debug('actual: {}'.format(repr(actual)))
1197+ for e in expected:
1198+ found = False
1199+ for act in actual:
1200+ a = {'enabled': act.enabled, 'name': act.name,
1201+ 'email': act.email, 'tenantId': act.tenantId,
1202+ 'id': act.id}
1203+ if e['name'] == a['name']:
1204+ found = True
1205+ ret = self._validate_dict_data(e, a)
1206+ if ret:
1207+ return "unexpected user data - {}".format(ret)
1208+ if not found:
1209+ return "user {} does not exist".format(e.name)
1210+ return ret
1211+
1212+ def validate_flavor_data(self, expected, actual):
1213+ """Validate a list of actual flavors vs a list of expected flavors."""
1214+ self.log.debug('actual: {}'.format(repr(actual)))
1215+ act = [a.name for a in actual]
1216+ return self._validate_list_data(expected, act)
1217+
1218+ def tenant_exists(self, keystone, tenant):
1219+ """Return True if tenant exists"""
1220+ return tenant in [t.name for t in keystone.tenants.list()]
1221+
1222+ def authenticate_keystone_admin(self, keystone_sentry, user, password,
1223+ tenant):
1224+ """Authenticates admin user with the keystone admin endpoint."""
1225+ service_ip = \
1226+ keystone_sentry.relation('shared-db',
1227+ 'mysql:shared-db')['private-address']
1228+ ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
1229+ return keystone_client.Client(username=user, password=password,
1230+ tenant_name=tenant, auth_url=ep)
1231+
1232+ def authenticate_keystone_user(self, keystone, user, password, tenant):
1233+ """Authenticates a regular user with the keystone public endpoint."""
1234+ ep = keystone.service_catalog.url_for(service_type='identity',
1235+ endpoint_type='publicURL')
1236+ return keystone_client.Client(username=user, password=password,
1237+ tenant_name=tenant, auth_url=ep)
1238+
1239+ def authenticate_glance_admin(self, keystone):
1240+ """Authenticates admin user with glance."""
1241+ ep = keystone.service_catalog.url_for(service_type='image',
1242+ endpoint_type='adminURL')
1243+ return glance_client.Client(ep, token=keystone.auth_token)
1244+
1245+ def authenticate_nova_user(self, keystone, user, password, tenant):
1246+ """Authenticates a regular user with nova-api."""
1247+ ep = keystone.service_catalog.url_for(service_type='identity',
1248+ endpoint_type='publicURL')
1249+ return nova_client.Client(username=user, api_key=password,
1250+ project_id=tenant, auth_url=ep)

Subscribers

People subscribed via source and target branches