Merge lp:~corey.bryant/charms/trusty/keystone/amulet-basic into lp:~openstack-charmers-archive/charms/trusty/keystone/trunk
- Trusty Tahr (14.04)
- amulet-basic
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
James Page | Needs Fixing | ||
Review via email:
|
This proposal has been superseded by a proposal from 2014-06-20.
Commit message
Description of the change
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
James Page (james-page) wrote : | # |
- 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
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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!
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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 OpenStackAmulet
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Corey Bryant (corey.bryant) wrote : | # |
Hi James,
Thanks for the review. I might just keep AmuletDeployment as a more generic class and create OpenStackAmulet
Off I go to rework.
Corey
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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_
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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:/
*amulet_utils.py - Change get_logger() defaults.
*amulet_utils.py - Added validate_
*amulet_utils.py - Added -f option to pgrep in _get_proc_
*amulet_utils.py - Added more code to validate_
*basic_
*basic_
*basic_
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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
*amulet_utils.py - Changed sleep(5) to sleep(10) in
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Corey Bryant (corey.bryant) wrote : | # |
I've pushed a new version with the following updates:
*tests/README - Added README file
*tests/
*tests/
*tests/
*tests/
*tests/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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
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) |
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 = KeyStoneDeploym ent(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.