Merge lp:~1chb1n/charms/trusty/glance/next-amulet-update into lp:~openstack-charmers-archive/charms/trusty/glance/next

Proposed by Ryan Beisner
Status: Merged
Merged at revision: 122
Proposed branch: lp:~1chb1n/charms/trusty/glance/next-amulet-update
Merge into: lp:~openstack-charmers-archive/charms/trusty/glance/next
Diff against target: 1784 lines (+929/-460)
13 files modified
Makefile (+10/-15)
hooks/charmhelpers/core/hookenv.py (+92/-36)
hooks/charmhelpers/core/services/base.py (+12/-9)
metadata.yaml (+4/-2)
tests/00-setup (+6/-2)
tests/020-basic-trusty-liberty (+11/-0)
tests/021-basic-wily-liberty (+9/-0)
tests/README (+9/-0)
tests/basic_deployment.py (+349/-340)
tests/charmhelpers/contrib/amulet/utils.py (+137/-4)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+35/-3)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+237/-49)
tests/tests.yaml (+18/-0)
To merge this branch: bzr merge lp:~1chb1n/charms/trusty/glance/next-amulet-update
Reviewer Review Type Date Requested Status
Corey Bryant Pending
Review via email: mp+263413@code.launchpad.net

This proposal supersedes a proposal from 2015-06-30.

Description of the change

Update amulet tests for Kilo, prep for wily. Sync hooks/charmhelpers; Sync tests/charmhelpers.

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

charm_lint_check #5714 glance-next for 1chb1n mp263413
    LINT FAIL: lint-test failed

LINT Results (max last 2 lines):
make: *** [lint] Error 1
ERROR:root:Make target returned non-zero.

Full lint test output: http://paste.ubuntu.com/11808187/
Build: http://10.245.162.77:8080/job/charm_lint_check/5714/

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

charm_unit_test #5346 glance-next for 1chb1n mp263413
    UNIT OK: passed

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

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

charm_lint_check #5716 glance-next for 1chb1n mp263413
    LINT OK: passed

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

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

charm_unit_test #5348 glance-next for 1chb1n mp263413
    UNIT OK: passed

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

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

charm_amulet_test #4906 glance-next for 1chb1n mp263413
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
make: *** [functional_test] Error 1
ERROR:root:Make target returned non-zero.

Full amulet test output: http://paste.ubuntu.com/11808398/
Build: http://10.245.162.77:8080/job/charm_amulet_test/4906/

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

charm_amulet_test #4908 glance-next for 1chb1n mp263413
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
make: *** [functional_test] Error 1
ERROR:root:Make target returned non-zero.

Full amulet test output: http://paste.ubuntu.com/11808501/
Build: http://10.245.162.77:8080/job/charm_amulet_test/4908/

126. By Ryan Beisner

update tests for vivid-kilo

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

charm_lint_check #5718 glance-next for 1chb1n mp263413
    LINT OK: passed

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

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

charm_unit_test #5350 glance-next for 1chb1n mp263413
    UNIT OK: passed

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

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

charm_amulet_test #4910 glance-next for 1chb1n mp263413
    AMULET OK: passed

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'Makefile'
--- Makefile 2015-04-16 21:32:02 +0000
+++ Makefile 2015-07-02 12:52:15 +0000
@@ -2,16 +2,18 @@
2PYTHON := /usr/bin/env python2PYTHON := /usr/bin/env python
33
4lint:4lint:
5 @echo "Running flake8 tests: "5 @flake8 --exclude hooks/charmhelpers,tests/charmhelpers \
6 @flake8 --exclude hooks/charmhelpers actions hooks unit_tests tests6 actions hooks unit_tests tests
7 @echo "OK"
8 @echo "Running charm proof: "
9 @charm proof7 @charm proof
10 @echo "OK"
118
12unit_test:9test:
10 @# Bundletester expects unit tests here.
13 @$(PYTHON) /usr/bin/nosetests --nologcapture --with-coverage unit_tests11 @$(PYTHON) /usr/bin/nosetests --nologcapture --with-coverage unit_tests
1412
13functional_test:
14 @echo Starting Amulet tests...
15 @juju test -v -p AMULET_HTTP_PROXY,AMULET_OS_VIP --timeout 2700
16
15bin/charm_helpers_sync.py:17bin/charm_helpers_sync.py:
16 @mkdir -p bin18 @mkdir -p bin
17 @bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \19 @bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \
@@ -21,15 +23,8 @@
21 @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-hooks.yaml23 @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-hooks.yaml
22 @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-tests.yaml24 @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-tests.yaml
2325
24test:26publish: lint test
25 @echo Starting Amulet tests...
26 # /!\ Note: The -v should only be temporary until Amulet sends
27 # raise_status() messages to stderr:
28 # https://bugs.launchpad.net/amulet/+bug/1320357
29 @juju test -v -p AMULET_HTTP_PROXY,AMULET_OS_VIP --timeout 2700
30
31publish: lint unit_test
32 bzr push lp:charms/glance27 bzr push lp:charms/glance
33 bzr push lp:charms/trusty/glance28 bzr push lp:charms/trusty/glance
3429
35all: unit_test lint30all: test lint
3631
=== modified file 'hooks/charmhelpers/core/hookenv.py'
--- hooks/charmhelpers/core/hookenv.py 2015-06-10 20:31:46 +0000
+++ hooks/charmhelpers/core/hookenv.py 2015-07-02 12:52:15 +0000
@@ -21,7 +21,9 @@
21# Charm Helpers Developers <juju@lists.ubuntu.com>21# Charm Helpers Developers <juju@lists.ubuntu.com>
2222
23from __future__ import print_function23from __future__ import print_function
24from distutils.version import LooseVersion
24from functools import wraps25from functools import wraps
26import glob
25import os27import os
26import json28import json
27import yaml29import yaml
@@ -242,29 +244,7 @@
242 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)244 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
243 if os.path.exists(self.path):245 if os.path.exists(self.path):
244 self.load_previous()246 self.load_previous()
245247 atexit(self._implicit_save)
246 def __getitem__(self, key):
247 """For regular dict lookups, check the current juju config first,
248 then the previous (saved) copy. This ensures that user-saved values
249 will be returned by a dict lookup.
250
251 """
252 try:
253 return dict.__getitem__(self, key)
254 except KeyError:
255 return (self._prev_dict or {})[key]
256
257 def get(self, key, default=None):
258 try:
259 return self[key]
260 except KeyError:
261 return default
262
263 def keys(self):
264 prev_keys = []
265 if self._prev_dict is not None:
266 prev_keys = self._prev_dict.keys()
267 return list(set(prev_keys + list(dict.keys(self))))
268248
269 def load_previous(self, path=None):249 def load_previous(self, path=None):
270 """Load previous copy of config from disk.250 """Load previous copy of config from disk.
@@ -283,6 +263,9 @@
283 self.path = path or self.path263 self.path = path or self.path
284 with open(self.path) as f:264 with open(self.path) as f:
285 self._prev_dict = json.load(f)265 self._prev_dict = json.load(f)
266 for k, v in self._prev_dict.items():
267 if k not in self:
268 self[k] = v
286269
287 def changed(self, key):270 def changed(self, key):
288 """Return True if the current value for this key is different from271 """Return True if the current value for this key is different from
@@ -314,13 +297,13 @@
314 instance.297 instance.
315298
316 """299 """
317 if self._prev_dict:
318 for k, v in six.iteritems(self._prev_dict):
319 if k not in self:
320 self[k] = v
321 with open(self.path, 'w') as f:300 with open(self.path, 'w') as f:
322 json.dump(self, f)301 json.dump(self, f)
323302
303 def _implicit_save(self):
304 if self.implicit_save:
305 self.save()
306
324307
325@cached308@cached
326def config(scope=None):309def config(scope=None):
@@ -587,10 +570,14 @@
587 hooks.execute(sys.argv)570 hooks.execute(sys.argv)
588 """571 """
589572
590 def __init__(self, config_save=True):573 def __init__(self, config_save=None):
591 super(Hooks, self).__init__()574 super(Hooks, self).__init__()
592 self._hooks = {}575 self._hooks = {}
593 self._config_save = config_save576
577 # For unknown reasons, we allow the Hooks constructor to override
578 # config().implicit_save.
579 if config_save is not None:
580 config().implicit_save = config_save
594581
595 def register(self, name, function):582 def register(self, name, function):
596 """Register a hook"""583 """Register a hook"""
@@ -598,13 +585,16 @@
598585
599 def execute(self, args):586 def execute(self, args):
600 """Execute a registered hook based on args[0]"""587 """Execute a registered hook based on args[0]"""
588 _run_atstart()
601 hook_name = os.path.basename(args[0])589 hook_name = os.path.basename(args[0])
602 if hook_name in self._hooks:590 if hook_name in self._hooks:
603 self._hooks[hook_name]()591 try:
604 if self._config_save:592 self._hooks[hook_name]()
605 cfg = config()593 except SystemExit as x:
606 if cfg.implicit_save:594 if x.code is None or x.code == 0:
607 cfg.save()595 _run_atexit()
596 raise
597 _run_atexit()
608 else:598 else:
609 raise UnregisteredHookError(hook_name)599 raise UnregisteredHookError(hook_name)
610600
@@ -732,13 +722,79 @@
732@translate_exc(from_exc=OSError, to_exc=NotImplementedError)722@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
733def leader_set(settings=None, **kwargs):723def leader_set(settings=None, **kwargs):
734 """Juju leader set value(s)"""724 """Juju leader set value(s)"""
735 log("Juju leader-set '%s'" % (settings), level=DEBUG)725 # Don't log secrets.
726 # log("Juju leader-set '%s'" % (settings), level=DEBUG)
736 cmd = ['leader-set']727 cmd = ['leader-set']
737 settings = settings or {}728 settings = settings or {}
738 settings.update(kwargs)729 settings.update(kwargs)
739 for k, v in settings.iteritems():730 for k, v in settings.items():
740 if v is None:731 if v is None:
741 cmd.append('{}='.format(k))732 cmd.append('{}='.format(k))
742 else:733 else:
743 cmd.append('{}={}'.format(k, v))734 cmd.append('{}={}'.format(k, v))
744 subprocess.check_call(cmd)735 subprocess.check_call(cmd)
736
737
738@cached
739def juju_version():
740 """Full version string (eg. '1.23.3.1-trusty-amd64')"""
741 # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1
742 jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0]
743 return subprocess.check_output([jujud, 'version'],
744 universal_newlines=True).strip()
745
746
747@cached
748def has_juju_version(minimum_version):
749 """Return True if the Juju version is at least the provided version"""
750 return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
751
752
753_atexit = []
754_atstart = []
755
756
757def atstart(callback, *args, **kwargs):
758 '''Schedule a callback to run before the main hook.
759
760 Callbacks are run in the order they were added.
761
762 This is useful for modules and classes to perform initialization
763 and inject behavior. In particular:
764 - Run common code before all of your hooks, such as logging
765 the hook name or interesting relation data.
766 - Defer object or module initialization that requires a hook
767 context until we know there actually is a hook context,
768 making testing easier.
769 - Rather than requiring charm authors to include boilerplate to
770 invoke your helper's behavior, have it run automatically if
771 your object is instantiated or module imported.
772
773 This is not at all useful after your hook framework as been launched.
774 '''
775 global _atstart
776 _atstart.append((callback, args, kwargs))
777
778
779def atexit(callback, *args, **kwargs):
780 '''Schedule a callback to run on successful hook completion.
781
782 Callbacks are run in the reverse order that they were added.'''
783 _atexit.append((callback, args, kwargs))
784
785
786def _run_atstart():
787 '''Hook frameworks must invoke this before running the main hook body.'''
788 global _atstart
789 for callback, args, kwargs in _atstart:
790 callback(*args, **kwargs)
791 del _atstart[:]
792
793
794def _run_atexit():
795 '''Hook frameworks must invoke this after the main hook body has
796 successfully completed. Do not invoke it if the hook fails.'''
797 global _atexit
798 for callback, args, kwargs in reversed(_atexit):
799 callback(*args, **kwargs)
800 del _atexit[:]
745801
=== modified file 'hooks/charmhelpers/core/services/base.py'
--- hooks/charmhelpers/core/services/base.py 2015-06-10 20:31:46 +0000
+++ hooks/charmhelpers/core/services/base.py 2015-07-02 12:52:15 +0000
@@ -128,15 +128,18 @@
128 """128 """
129 Handle the current hook by doing The Right Thing with the registered services.129 Handle the current hook by doing The Right Thing with the registered services.
130 """130 """
131 hook_name = hookenv.hook_name()131 hookenv._run_atstart()
132 if hook_name == 'stop':132 try:
133 self.stop_services()133 hook_name = hookenv.hook_name()
134 else:134 if hook_name == 'stop':
135 self.reconfigure_services()135 self.stop_services()
136 self.provide_data()136 else:
137 cfg = hookenv.config()137 self.reconfigure_services()
138 if cfg.implicit_save:138 self.provide_data()
139 cfg.save()139 except SystemExit as x:
140 if x.code is None or x.code == 0:
141 hookenv._run_atexit()
142 hookenv._run_atexit()
140143
141 def provide_data(self):144 def provide_data(self):
142 """145 """
143146
=== modified file 'metadata.yaml'
--- metadata.yaml 2014-10-30 03:30:35 +0000
+++ metadata.yaml 2015-07-02 12:52:15 +0000
@@ -6,8 +6,10 @@
6 (Parallax) and an image delivery service (Teller). These services are used6 (Parallax) and an image delivery service (Teller). These services are used
7 in conjunction by Nova to deliver images from object stores, such as7 in conjunction by Nova to deliver images from object stores, such as
8 OpenStack's Swift service, to Nova's compute nodes.8 OpenStack's Swift service, to Nova's compute nodes.
9categories:9tags:
10 - miscellaneous10 - openstack
11 - storage
12 - misc
11provides:13provides:
12 nrpe-external-master:14 nrpe-external-master:
13 interface: nrpe-external-master15 interface: nrpe-external-master
1416
=== modified file 'tests/00-setup'
--- tests/00-setup 2014-10-08 20:18:38 +0000
+++ tests/00-setup 2015-07-02 12:52:15 +0000
@@ -5,6 +5,10 @@
5sudo add-apt-repository --yes ppa:juju/stable5sudo add-apt-repository --yes ppa:juju/stable
6sudo apt-get update --yes6sudo apt-get update --yes
7sudo apt-get install --yes python-amulet \7sudo apt-get install --yes python-amulet \
8 python-cinderclient \
9 python-distro-info \
10 python-glanceclient \
11 python-heatclient \
8 python-keystoneclient \12 python-keystoneclient \
9 python-glanceclient \13 python-novaclient \
10 python-novaclient14 python-swiftclient
1115
=== modified file 'tests/017-basic-trusty-kilo' (properties changed: -x to +x)
=== modified file 'tests/019-basic-vivid-kilo' (properties changed: -x to +x)
=== added file 'tests/020-basic-trusty-liberty'
--- tests/020-basic-trusty-liberty 1970-01-01 00:00:00 +0000
+++ tests/020-basic-trusty-liberty 2015-07-02 12:52:15 +0000
@@ -0,0 +1,11 @@
1#!/usr/bin/python
2
3"""Amulet tests on a basic glance deployment on trusty-liberty."""
4
5from basic_deployment import GlanceBasicDeployment
6
7if __name__ == '__main__':
8 deployment = GlanceBasicDeployment(series='trusty',
9 openstack='cloud:trusty-liberty',
10 source='cloud:trusty-updates/liberty')
11 deployment.run_tests()
012
=== added file 'tests/021-basic-wily-liberty'
--- tests/021-basic-wily-liberty 1970-01-01 00:00:00 +0000
+++ tests/021-basic-wily-liberty 2015-07-02 12:52:15 +0000
@@ -0,0 +1,9 @@
1#!/usr/bin/python
2
3"""Amulet tests on a basic glance deployment on wily-liberty."""
4
5from basic_deployment import GlanceBasicDeployment
6
7if __name__ == '__main__':
8 deployment = GlanceBasicDeployment(series='wily')
9 deployment.run_tests()
010
=== modified file 'tests/README'
--- tests/README 2014-10-08 20:18:38 +0000
+++ tests/README 2015-07-02 12:52:15 +0000
@@ -1,6 +1,15 @@
1This directory provides Amulet tests that focus on verification of Glance1This directory provides Amulet tests that focus on verification of Glance
2deployments.2deployments.
33
4test_* methods are called in lexical sort order.
5
6Test name convention to ensure desired test order:
7 1xx service and endpoint checks
8 2xx relation checks
9 3xx config checks
10 4xx functional checks
11 9xx restarts and other final checks
12
4In order to run tests, you'll need charm-tools installed (in addition to13In order to run tests, you'll need charm-tools installed (in addition to
5juju, of course):14juju, of course):
6 sudo add-apt-repository ppa:juju/stable15 sudo add-apt-repository ppa:juju/stable
716
=== modified file 'tests/basic_deployment.py'
--- tests/basic_deployment.py 2015-05-12 14:49:27 +0000
+++ tests/basic_deployment.py 2015-07-02 12:52:15 +0000
@@ -1,7 +1,12 @@
1#!/usr/bin/python1#!/usr/bin/python
22
3"""
4Basic glance amulet functional tests.
5"""
6
3import amulet7import amulet
4import os8import os
9import time
5import yaml10import yaml
611
7from charmhelpers.contrib.openstack.amulet.deployment import (12from charmhelpers.contrib.openstack.amulet.deployment import (
@@ -10,25 +15,24 @@
1015
11from charmhelpers.contrib.openstack.amulet.utils import (16from charmhelpers.contrib.openstack.amulet.utils import (
12 OpenStackAmuletUtils,17 OpenStackAmuletUtils,
13 DEBUG, # flake8: noqa18 DEBUG,
14 ERROR19 # ERROR
15)20)
1621
17# Use DEBUG to turn on debug logging22# Use DEBUG to turn on debug logging
18u = OpenStackAmuletUtils(DEBUG)23u = OpenStackAmuletUtils(DEBUG)
1924
25
20class GlanceBasicDeployment(OpenStackAmuletDeployment):26class GlanceBasicDeployment(OpenStackAmuletDeployment):
21 '''Amulet tests on a basic file-backed glance deployment. Verify relations,27 """Amulet tests on a basic file-backed glance deployment. Verify
22 service status, endpoint service catalog, create and delete new image.'''28 relations, service status, endpoint service catalog, create and
2329 delete new image."""
24# TO-DO(beisner):
25# * Add tests with different storage back ends
26# * Resolve Essex->Havana juju set charm bug
2730
28 def __init__(self, series=None, openstack=None, source=None, git=False,31 def __init__(self, series=None, openstack=None, source=None, git=False,
29 stable=False):32 stable=False):
30 '''Deploy the entire test environment.'''33 """Deploy the entire test environment."""
31 super(GlanceBasicDeployment, self).__init__(series, openstack, source, stable)34 super(GlanceBasicDeployment, self).__init__(series, openstack,
35 source, stable)
32 self.git = git36 self.git = git
33 self._add_services()37 self._add_services()
34 self._add_relations()38 self._add_relations()
@@ -37,20 +41,21 @@
37 self._initialize_tests()41 self._initialize_tests()
3842
39 def _add_services(self):43 def _add_services(self):
40 '''Add services44 """Add services
4145
42 Add the services that we're testing, where glance is local,46 Add the services that we're testing, where glance is local,
43 and the rest of the service are from lp branches that are47 and the rest of the service are from lp branches that are
44 compatible with the local charm (e.g. stable or next).48 compatible with the local charm (e.g. stable or next).
45 '''49 """
46 this_service = {'name': 'glance'}50 this_service = {'name': 'glance'}
47 other_services = [{'name': 'mysql'}, {'name': 'rabbitmq-server'},51 other_services = [{'name': 'mysql'},
52 {'name': 'rabbitmq-server'},
48 {'name': 'keystone'}]53 {'name': 'keystone'}]
49 super(GlanceBasicDeployment, self)._add_services(this_service,54 super(GlanceBasicDeployment, self)._add_services(this_service,
50 other_services)55 other_services)
5156
52 def _add_relations(self):57 def _add_relations(self):
53 '''Add relations for the services.'''58 """Add relations for the services."""
54 relations = {'glance:identity-service': 'keystone:identity-service',59 relations = {'glance:identity-service': 'keystone:identity-service',
55 'glance:shared-db': 'mysql:shared-db',60 'glance:shared-db': 'mysql:shared-db',
56 'keystone:shared-db': 'mysql:shared-db',61 'keystone:shared-db': 'mysql:shared-db',
@@ -58,7 +63,7 @@
58 super(GlanceBasicDeployment, self)._add_relations(relations)63 super(GlanceBasicDeployment, self)._add_relations(relations)
5964
60 def _configure_services(self):65 def _configure_services(self):
61 '''Configure all of the services.'''66 """Configure all of the services."""
62 glance_config = {}67 glance_config = {}
63 if self.git:68 if self.git:
64 branch = 'stable/' + self._get_openstack_release_string()69 branch = 'stable/' + self._get_openstack_release_string()
@@ -76,7 +81,8 @@
76 'http_proxy': amulet_http_proxy,81 'http_proxy': amulet_http_proxy,
77 'https_proxy': amulet_http_proxy,82 'https_proxy': amulet_http_proxy,
78 }83 }
79 glance_config['openstack-origin-git'] = yaml.dump(openstack_origin_git)84 glance_config['openstack-origin-git'] = \
85 yaml.dump(openstack_origin_git)
8086
81 keystone_config = {'admin-password': 'openstack',87 keystone_config = {'admin-password': 'openstack',
82 'admin-token': 'ubuntutesting'}88 'admin-token': 'ubuntutesting'}
@@ -87,12 +93,19 @@
87 super(GlanceBasicDeployment, self)._configure_services(configs)93 super(GlanceBasicDeployment, self)._configure_services(configs)
8894
89 def _initialize_tests(self):95 def _initialize_tests(self):
90 '''Perform final initialization before tests get run.'''96 """Perform final initialization before tests get run."""
91 # Access the sentries for inspecting service units97 # Access the sentries for inspecting service units
92 self.mysql_sentry = self.d.sentry.unit['mysql/0']98 self.mysql_sentry = self.d.sentry.unit['mysql/0']
93 self.glance_sentry = self.d.sentry.unit['glance/0']99 self.glance_sentry = self.d.sentry.unit['glance/0']
94 self.keystone_sentry = self.d.sentry.unit['keystone/0']100 self.keystone_sentry = self.d.sentry.unit['keystone/0']
95 self.rabbitmq_sentry = self.d.sentry.unit['rabbitmq-server/0']101 self.rabbitmq_sentry = self.d.sentry.unit['rabbitmq-server/0']
102 u.log.debug('openstack release val: {}'.format(
103 self._get_openstack_release()))
104 u.log.debug('openstack release str: {}'.format(
105 self._get_openstack_release_string()))
106
107 # Let things settle a bit before moving forward
108 time.sleep(30)
96109
97 # Authenticate admin with keystone110 # Authenticate admin with keystone
98 self.keystone = u.authenticate_keystone_admin(self.keystone_sentry,111 self.keystone = u.authenticate_keystone_admin(self.keystone_sentry,
@@ -103,46 +116,103 @@
103 # Authenticate admin with glance endpoint116 # Authenticate admin with glance endpoint
104 self.glance = u.authenticate_glance_admin(self.keystone)117 self.glance = u.authenticate_glance_admin(self.keystone)
105118
106 u.log.debug('openstack release: {}'.format(self._get_openstack_release()))119 def test_100_services(self):
107120 """Verify that the expected services are running on the
108 def test_services(self):121 corresponding service units."""
109 '''Verify that the expected services are running on the122 services = {
110 corresponding service units.'''123 self.mysql_sentry: ['mysql'],
111 commands = {124 self.keystone_sentry: ['keystone'],
112 self.mysql_sentry: ['status mysql'],125 self.glance_sentry: ['glance-api', 'glance-registry'],
113 self.keystone_sentry: ['status keystone'],126 self.rabbitmq_sentry: ['rabbitmq-server']
114 self.glance_sentry: ['status glance-api', 'status glance-registry'],
115 self.rabbitmq_sentry: ['sudo service rabbitmq-server status']
116 }127 }
117 u.log.debug('commands: {}'.format(commands))128
118 ret = u.validate_services(commands)129 ret = u.validate_services_by_name(services)
119 if ret:130 if ret:
120 amulet.raise_status(amulet.FAIL, msg=ret)131 amulet.raise_status(amulet.FAIL, msg=ret)
121132
122 def test_service_catalog(self):133 def test_102_service_catalog(self):
123 '''Verify that the service catalog endpoint data'''134 """Verify that the service catalog endpoint data is valid."""
124 endpoint_vol = {'adminURL': u.valid_url,135 u.log.debug('Checking keystone service catalog...')
125 'region': 'RegionOne',136 endpoint_check = {
126 'publicURL': u.valid_url,137 'adminURL': u.valid_url,
127 'internalURL': u.valid_url}138 'id': u.not_null,
128 endpoint_id = {'adminURL': u.valid_url,139 'region': 'RegionOne',
129 'region': 'RegionOne',140 'publicURL': u.valid_url,
130 'publicURL': u.valid_url,141 'internalURL': u.valid_url
131 'internalURL': u.valid_url}142 }
132 if self._get_openstack_release() >= self.trusty_icehouse:143 expected = {
133 endpoint_vol['id'] = u.not_null144 'image': [endpoint_check],
134 endpoint_id['id'] = u.not_null145 'identity': [endpoint_check]
135146 }
136 expected = {'image': [endpoint_id],
137 'identity': [endpoint_id]}
138 actual = self.keystone.service_catalog.get_endpoints()147 actual = self.keystone.service_catalog.get_endpoints()
139148
140 ret = u.validate_svc_catalog_endpoint_data(expected, actual)149 ret = u.validate_svc_catalog_endpoint_data(expected, actual)
141 if ret:150 if ret:
142 amulet.raise_status(amulet.FAIL, msg=ret)151 amulet.raise_status(amulet.FAIL, msg=ret)
143152
144 def test_mysql_glance_db_relation(self):153 def test_104_glance_endpoint(self):
145 '''Verify the mysql:glance shared-db relation data'''154 """Verify the glance endpoint data."""
155 u.log.debug('Checking glance api endpoint data...')
156 endpoints = self.keystone.endpoints.list()
157 admin_port = internal_port = public_port = '9292'
158 expected = {
159 'id': u.not_null,
160 'region': 'RegionOne',
161 'adminurl': u.valid_url,
162 'internalurl': u.valid_url,
163 'publicurl': u.valid_url,
164 'service_id': u.not_null
165 }
166 ret = u.validate_endpoint_data(endpoints, admin_port, internal_port,
167 public_port, expected)
168
169 if ret:
170 amulet.raise_status(amulet.FAIL,
171 msg='glance endpoint: {}'.format(ret))
172
173 def test_106_keystone_endpoint(self):
174 """Verify the keystone endpoint data."""
175 u.log.debug('Checking keystone api endpoint data...')
176 endpoints = self.keystone.endpoints.list()
177 admin_port = '35357'
178 internal_port = public_port = '5000'
179 expected = {
180 'id': u.not_null,
181 'region': 'RegionOne',
182 'adminurl': u.valid_url,
183 'internalurl': u.valid_url,
184 'publicurl': u.valid_url,
185 'service_id': u.not_null
186 }
187 ret = u.validate_endpoint_data(endpoints, admin_port, internal_port,
188 public_port, expected)
189 if ret:
190 amulet.raise_status(amulet.FAIL,
191 msg='keystone endpoint: {}'.format(ret))
192
193 def test_110_users(self):
194 """Verify expected users."""
195 u.log.debug('Checking keystone users...')
196 expected = [
197 {'name': 'glance',
198 'enabled': True,
199 'tenantId': u.not_null,
200 'id': u.not_null,
201 'email': 'juju@localhost'},
202 {'name': 'admin',
203 'enabled': True,
204 'tenantId': u.not_null,
205 'id': u.not_null,
206 'email': 'juju@localhost'}
207 ]
208 actual = self.keystone.users.list()
209 ret = u.validate_user_data(expected, actual)
210 if ret:
211 amulet.raise_status(amulet.FAIL, msg=ret)
212
213 def test_200_mysql_glance_db_relation(self):
214 """Verify the mysql:glance shared-db relation data"""
215 u.log.debug('Checking mysql to glance shared-db relation data...')
146 unit = self.mysql_sentry216 unit = self.mysql_sentry
147 relation = ['shared-db', 'glance:shared-db']217 relation = ['shared-db', 'glance:shared-db']
148 expected = {218 expected = {
@@ -154,8 +224,9 @@
154 message = u.relation_error('mysql shared-db', ret)224 message = u.relation_error('mysql shared-db', ret)
155 amulet.raise_status(amulet.FAIL, msg=message)225 amulet.raise_status(amulet.FAIL, msg=message)
156226
157 def test_glance_mysql_db_relation(self):227 def test_201_glance_mysql_db_relation(self):
158 '''Verify the glance:mysql shared-db relation data'''228 """Verify the glance:mysql shared-db relation data"""
229 u.log.debug('Checking glance to mysql shared-db relation data...')
159 unit = self.glance_sentry230 unit = self.glance_sentry
160 relation = ['shared-db', 'mysql:shared-db']231 relation = ['shared-db', 'mysql:shared-db']
161 expected = {232 expected = {
@@ -169,8 +240,9 @@
169 message = u.relation_error('glance shared-db', ret)240 message = u.relation_error('glance shared-db', ret)
170 amulet.raise_status(amulet.FAIL, msg=message)241 amulet.raise_status(amulet.FAIL, msg=message)
171242
172 def test_keystone_glance_id_relation(self):243 def test_202_keystone_glance_id_relation(self):
173 '''Verify the keystone:glance identity-service relation data'''244 """Verify the keystone:glance identity-service relation data"""
245 u.log.debug('Checking keystone to glance id relation data...')
174 unit = self.keystone_sentry246 unit = self.keystone_sentry
175 relation = ['identity-service',247 relation = ['identity-service',
176 'glance:identity-service']248 'glance:identity-service']
@@ -193,8 +265,9 @@
193 message = u.relation_error('keystone identity-service', ret)265 message = u.relation_error('keystone identity-service', ret)
194 amulet.raise_status(amulet.FAIL, msg=message)266 amulet.raise_status(amulet.FAIL, msg=message)
195267
196 def test_glance_keystone_id_relation(self):268 def test_203_glance_keystone_id_relation(self):
197 '''Verify the glance:keystone identity-service relation data'''269 """Verify the glance:keystone identity-service relation data"""
270 u.log.debug('Checking glance to keystone relation data...')
198 unit = self.glance_sentry271 unit = self.glance_sentry
199 relation = ['identity-service',272 relation = ['identity-service',
200 'keystone:identity-service']273 'keystone:identity-service']
@@ -211,8 +284,9 @@
211 message = u.relation_error('glance identity-service', ret)284 message = u.relation_error('glance identity-service', ret)
212 amulet.raise_status(amulet.FAIL, msg=message)285 amulet.raise_status(amulet.FAIL, msg=message)
213286
214 def test_rabbitmq_glance_amqp_relation(self):287 def test_204_rabbitmq_glance_amqp_relation(self):
215 '''Verify the rabbitmq-server:glance amqp relation data'''288 """Verify the rabbitmq-server:glance amqp relation data"""
289 u.log.debug('Checking rmq to glance amqp relation data...')
216 unit = self.rabbitmq_sentry290 unit = self.rabbitmq_sentry
217 relation = ['amqp', 'glance:amqp']291 relation = ['amqp', 'glance:amqp']
218 expected = {292 expected = {
@@ -225,8 +299,9 @@
225 message = u.relation_error('rabbitmq amqp', ret)299 message = u.relation_error('rabbitmq amqp', ret)
226 amulet.raise_status(amulet.FAIL, msg=message)300 amulet.raise_status(amulet.FAIL, msg=message)
227301
228 def test_glance_rabbitmq_amqp_relation(self):302 def test_205_glance_rabbitmq_amqp_relation(self):
229 '''Verify the glance:rabbitmq-server amqp relation data'''303 """Verify the glance:rabbitmq-server amqp relation data"""
304 u.log.debug('Checking glance to rmq amqp relation data...')
230 unit = self.glance_sentry305 unit = self.glance_sentry
231 relation = ['amqp', 'rabbitmq-server:amqp']306 relation = ['amqp', 'rabbitmq-server:amqp']
232 expected = {307 expected = {
@@ -239,291 +314,225 @@
239 message = u.relation_error('glance amqp', ret)314 message = u.relation_error('glance amqp', ret)
240 amulet.raise_status(amulet.FAIL, msg=message)315 amulet.raise_status(amulet.FAIL, msg=message)
241316
242 def test_image_create_delete(self):317 def _get_keystone_authtoken_expected_dict(self, rel_ks_gl):
243 '''Create new cirros image in glance, verify, then delete it'''318 """Return expected authtoken dict for OS release"""
244319 expected = {
245 # Create a new image320 'keystone_authtoken': {
246 image_name = 'cirros-image-1'321 'signing_dir': '/var/cache/glance',
247 image_new = u.create_cirros_image(self.glance, image_name)322 'admin_tenant_name': 'services',
248323 'admin_user': 'glance',
249 # Confirm image is created and has status of 'active' 324 'admin_password': rel_ks_gl['service_password'],
250 if not image_new:325 'auth_uri': u.valid_url
251 message = 'glance image create failed'326 }
252 amulet.raise_status(amulet.FAIL, msg=message)327 }
253328
254 # Verify new image name329 if self._get_openstack_release() >= self.trusty_kilo:
255 images_list = list(self.glance.images.list())330 # Trusty-Kilo and later
256 if images_list[0].name != image_name:331 expected['keystone_authtoken'].update({
257 message = 'glance image create failed or unexpected image name {}'.format(images_list[0].name)332 'identity_uri': u.valid_url,
258 amulet.raise_status(amulet.FAIL, msg=message)333 })
259334 else:
260 # Delete the new image335 # Utopic-Juno and earlier
261 u.log.debug('image count before delete: {}'.format(len(list(self.glance.images.list()))))336 expected['keystone_authtoken'].update({
262 u.delete_image(self.glance, image_new)337 'auth_host': rel_ks_gl['auth_host'],
263 u.log.debug('image count after delete: {}'.format(len(list(self.glance.images.list()))))338 'auth_port': rel_ks_gl['auth_port'],
264339 'auth_protocol': rel_ks_gl['auth_protocol']
265 def test_glance_api_default_config(self):340 })
266 '''Verify default section configs in glance-api.conf and341
267 compare some of the parameters to relation data.'''342 return expected
268 unit = self.glance_sentry343
269 rel_gl_mq = unit.relation('amqp', 'rabbitmq-server:amqp')344 def test_300_glance_api_default_config(self):
270 conf = '/etc/glance/glance-api.conf'345 """Verify default section configs in glance-api.conf and
271 expected = {'use_syslog': 'False',346 compare some of the parameters to relation data."""
272 'default_store': 'file',347 u.log.debug('Checking glance api config file...')
273 'filesystem_store_datadir': '/var/lib/glance/images/',348 unit = self.glance_sentry
274 'rabbit_userid': rel_gl_mq['username'],349 unit_ks = self.keystone_sentry
275 'log_file': '/var/log/glance/api.log',350 rel_mq_gl = self.rabbitmq_sentry.relation('amqp', 'glance:amqp')
276 'debug': 'False',351 rel_ks_gl = unit_ks.relation('identity-service',
277 'verbose': 'False'}352 'glance:identity-service')
278 section = 'DEFAULT'353 rel_my_gl = self.mysql_sentry.relation('shared-db', 'glance:shared-db')
279354 db_uri = "mysql://{}:{}@{}/{}".format('glance', rel_my_gl['password'],
280 if self._get_openstack_release() <= self.precise_havana:355 rel_my_gl['db_host'], 'glance')
281 # Defaults were different before icehouse356 conf = '/etc/glance/glance-api.conf'
282 expected['debug'] = 'True'357 expected = {
283 expected['verbose'] = 'True'358 'DEFAULT': {
284359 'debug': 'False',
285 ret = u.validate_config_data(unit, conf, section, expected)360 'verbose': 'False',
286 if ret:361 'use_syslog': 'False',
287 message = "glance-api default config error: {}".format(ret)362 'log_file': '/var/log/glance/api.log',
288 amulet.raise_status(amulet.FAIL, msg=message)363 'bind_host': '0.0.0.0',
289364 'bind_port': '9282',
290 def test_glance_api_auth_config(self):365 'registry_host': '0.0.0.0',
291 '''Verify authtoken section config in glance-api.conf using366 'registry_port': '9191',
292 glance/keystone relation data.'''367 'registry_client_protocol': 'http',
293 unit_gl = self.glance_sentry368 'delayed_delete': 'False',
294 unit_ks = self.keystone_sentry369 'scrub_time': '43200',
295 rel_gl_mq = unit_gl.relation('amqp', 'rabbitmq-server:amqp')370 'notification_driver': 'rabbit',
296 rel_ks_gl = unit_ks.relation('identity-service', 'glance:identity-service')371 'scrubber_datadir': '/var/lib/glance/scrubber',
297 conf = '/etc/glance/glance-api.conf'372 'image_cache_dir': '/var/lib/glance/image-cache/',
298 section = 'keystone_authtoken'373 'db_enforce_mysql_charset': 'False'
299374 },
300 if self._get_openstack_release() > self.precise_havana:375 }
301 # No auth config exists in this file before icehouse376
302 expected = {'admin_user': 'glance',377 expected.update(self._get_keystone_authtoken_expected_dict(rel_ks_gl))
303 'admin_password': rel_ks_gl['service_password']}378
304379 if self._get_openstack_release() >= self.trusty_kilo:
305 ret = u.validate_config_data(unit_gl, conf, section, expected)380 # Kilo or later
306 if ret:381 expected['oslo_messaging_rabbit'] = {
307 message = "glance-api auth config error: {}".format(ret)382 'rabbit_userid': 'glance',
308 amulet.raise_status(amulet.FAIL, msg=message)383 'rabbit_virtual_host': 'openstack',
309384 'rabbit_password': rel_mq_gl['password'],
310 def test_glance_api_paste_auth_config(self):385 'rabbit_host': rel_mq_gl['hostname']
311 '''Verify authtoken section config in glance-api-paste.ini using386 }
312 glance/keystone relation data.'''387 expected['glance_store'] = {
313 unit_gl = self.glance_sentry388 'filesystem_store_datadir': '/var/lib/glance/images/',
314 unit_ks = self.keystone_sentry389 'stores': 'glance.store.filesystem.'
315 rel_gl_mq = unit_gl.relation('amqp', 'rabbitmq-server:amqp')390 'Store,glance.store.http.Store',
316 rel_ks_gl = unit_ks.relation('identity-service', 'glance:identity-service')391 'default_store': 'file'
392 }
393 expected['database'] = {
394 'idle_timeout': '3600',
395 'connection': db_uri
396 }
397 else:
398 # Juno or earlier
399 expected['DEFAULT'].update({
400 'rabbit_userid': 'glance',
401 'rabbit_virtual_host': 'openstack',
402 'rabbit_password': rel_mq_gl['password'],
403 'rabbit_host': rel_mq_gl['hostname'],
404 'filesystem_store_datadir': '/var/lib/glance/images/',
405 'default_store': 'file',
406 })
407 expected['database'] = {
408 'sql_idle_timeout': '3600',
409 'connection': db_uri
410 }
411
412 for section, pairs in expected.iteritems():
413 ret = u.validate_config_data(unit, conf, section, pairs)
414 if ret:
415 message = "glance api config error: {}".format(ret)
416 amulet.raise_status(amulet.FAIL, msg=message)
417
418 def test_302_glance_registry_default_config(self):
419 """Verify configs in glance-registry.conf"""
420 u.log.debug('Checking glance registry config file...')
421 unit = self.glance_sentry
422 unit_ks = self.keystone_sentry
423 rel_ks_gl = unit_ks.relation('identity-service',
424 'glance:identity-service')
425 rel_my_gl = self.mysql_sentry.relation('shared-db', 'glance:shared-db')
426 db_uri = "mysql://{}:{}@{}/{}".format('glance', rel_my_gl['password'],
427 rel_my_gl['db_host'], 'glance')
428 conf = '/etc/glance/glance-registry.conf'
429
430 expected = {
431 'DEFAULT': {
432 'use_syslog': 'False',
433 'log_file': '/var/log/glance/registry.log',
434 'debug': 'False',
435 'verbose': 'False',
436 'bind_host': '0.0.0.0',
437 'bind_port': '9191'
438 },
439 }
440
441 if self._get_openstack_release() >= self.trusty_kilo:
442 # Kilo or later
443 expected['database'] = {
444 'idle_timeout': '3600',
445 'connection': db_uri
446 }
447 else:
448 # Juno or earlier
449 expected['database'] = {
450 'idle_timeout': '3600',
451 'connection': db_uri
452 }
453
454 expected.update(self._get_keystone_authtoken_expected_dict(rel_ks_gl))
455
456 for section, pairs in expected.iteritems():
457 ret = u.validate_config_data(unit, conf, section, pairs)
458 if ret:
459 message = "glance registry paste config error: {}".format(ret)
460 amulet.raise_status(amulet.FAIL, msg=message)
461
462 def _get_filter_factory_expected_dict(self):
463 """Return expected authtoken filter factory dict for OS release"""
464 if self._get_openstack_release() >= self.trusty_kilo:
465 # Kilo and later
466 val = 'keystonemiddleware.auth_token:filter_factory'
467 else:
468 # Juno and earlier
469 val = 'keystoneclient.middleware.auth_token:filter_factory'
470
471 return {'filter:authtoken': {'paste.filter_factory': val}}
472
473 def test_304_glance_api_paste_auth_config(self):
474 """Verify authtoken section config in glance-api-paste.ini using
475 glance/keystone relation data."""
476 u.log.debug('Checking glance api paste config file...')
477 unit = self.glance_sentry
317 conf = '/etc/glance/glance-api-paste.ini'478 conf = '/etc/glance/glance-api-paste.ini'
318 section = 'filter:authtoken'479 expected = self._get_filter_factory_expected_dict()
319480
320 if self._get_openstack_release() <= self.precise_havana:481 for section, pairs in expected.iteritems():
321 # No auth config exists in this file after havana482 ret = u.validate_config_data(unit, conf, section, pairs)
322 expected = {'admin_user': 'glance',
323 'admin_password': rel_ks_gl['service_password']}
324
325 ret = u.validate_config_data(unit_gl, conf, section, expected)
326 if ret:483 if ret:
327 message = "glance-api-paste auth config error: {}".format(ret)484 message = "glance api paste config error: {}".format(ret)
328 amulet.raise_status(amulet.FAIL, msg=message)485 amulet.raise_status(amulet.FAIL, msg=message)
329486
330 def test_glance_registry_paste_auth_config(self):487 def test_306_glance_registry_paste_auth_config(self):
331 '''Verify authtoken section config in glance-registry-paste.ini using488 """Verify authtoken section config in glance-registry-paste.ini using
332 glance/keystone relation data.'''489 glance/keystone relation data."""
333 unit_gl = self.glance_sentry490 u.log.debug('Checking glance registry paste config file...')
334 unit_ks = self.keystone_sentry491 unit = self.glance_sentry
335 rel_gl_mq = unit_gl.relation('amqp', 'rabbitmq-server:amqp')
336 rel_ks_gl = unit_ks.relation('identity-service', 'glance:identity-service')
337 conf = '/etc/glance/glance-registry-paste.ini'492 conf = '/etc/glance/glance-registry-paste.ini'
338 section = 'filter:authtoken'493 expected = self._get_filter_factory_expected_dict()
339494
340 if self._get_openstack_release() <= self.precise_havana:495 for section, pairs in expected.iteritems():
341 # No auth config exists in this file after havana496 ret = u.validate_config_data(unit, conf, section, pairs)
342 expected = {'admin_user': 'glance',497 if ret:
343 'admin_password': rel_ks_gl['service_password']}498 message = "glance registry paste config error: {}".format(ret)
344499 amulet.raise_status(amulet.FAIL, msg=message)
345 ret = u.validate_config_data(unit_gl, conf, section, expected)500
346 if ret:501 def test_410_glance_image_create_delete(self):
347 message = "glance-registry-paste auth config error: {}".format(ret)502 """Create new cirros image in glance, verify, then delete it."""
348 amulet.raise_status(amulet.FAIL, msg=message)503 u.log.debug('Creating, checking and deleting glance image...')
349504 img_new = u.create_cirros_image(self.glance, "cirros-image-1")
350 def test_glance_registry_default_config(self):505 img_id = img_new.id
351 '''Verify default section configs in glance-registry.conf'''506 u.delete_resource(self.glance.images, img_id, msg="glance image")
352 unit = self.glance_sentry507
353 conf = '/etc/glance/glance-registry.conf'508 def test_900_glance_restart_on_config_change(self):
354 expected = {'use_syslog': 'False',509 """Verify that the specified services are restarted when the config
355 'log_file': '/var/log/glance/registry.log',510 is changed."""
356 'debug': 'False',511 sentry = self.glance_sentry
357 'verbose': 'False'}512 juju_service = 'glance'
358 section = 'DEFAULT'513
359514 # Expected default and alternate values
360 if self._get_openstack_release() <= self.precise_havana:515 set_default = {'use-syslog': 'False'}
361 # Defaults were different before icehouse516 set_alternate = {'use-syslog': 'True'}
362 expected['debug'] = 'True'517
363 expected['verbose'] = 'True'518 # Config file affected by juju set config change
364519 conf_file = '/etc/glance/glance-api.conf'
365 ret = u.validate_config_data(unit, conf, section, expected)520
366 if ret:521 # Services which are expected to restart upon config change
367 message = "glance-registry default config error: {}".format(ret)522 services = ['glance-api', 'glance-registry']
368 amulet.raise_status(amulet.FAIL, msg=message)523
369524 # Make config change, check for service restarts
370 def test_glance_registry_auth_config(self):525 u.log.debug('Making config change on {}...'.format(juju_service))
371 '''Verify authtoken section config in glance-registry.conf526 self.d.configure(juju_service, set_alternate)
372 using glance/keystone relation data.'''527
373 unit_gl = self.glance_sentry528 sleep_time = 30
374 unit_ks = self.keystone_sentry529 for s in services:
375 rel_gl_mq = unit_gl.relation('amqp', 'rabbitmq-server:amqp')530 u.log.debug("Checking that service restarted: {}".format(s))
376 rel_ks_gl = unit_ks.relation('identity-service', 'glance:identity-service')531 if not u.service_restarted(sentry, s,
377 conf = '/etc/glance/glance-registry.conf'532 conf_file, sleep_time=sleep_time):
378 section = 'keystone_authtoken'533 self.d.configure(juju_service, set_default)
379534 msg = "service {} didn't restart after config change".format(s)
380 if self._get_openstack_release() > self.precise_havana:535 amulet.raise_status(amulet.FAIL, msg=msg)
381 # No auth config exists in this file before icehouse536 sleep_time = 0
382 expected = {'admin_user': 'glance',537
383 'admin_password': rel_ks_gl['service_password']}538 self.d.configure(juju_service, set_default)
384
385 ret = u.validate_config_data(unit_gl, conf, section, expected)
386 if ret:
387 message = "glance-registry keystone_authtoken config error: {}".format(ret)
388 amulet.raise_status(amulet.FAIL, msg=message)
389
390 def test_glance_api_database_config(self):
391 '''Verify database config in glance-api.conf and
392 compare with a db uri constructed from relation data.'''
393 unit = self.glance_sentry
394 conf = '/etc/glance/glance-api.conf'
395 relation = self.mysql_sentry.relation('shared-db', 'glance:shared-db')
396 db_uri = "mysql://{}:{}@{}/{}".format('glance', relation['password'],
397 relation['db_host'], 'glance')
398 expected = {'connection': db_uri, 'sql_idle_timeout': '3600'}
399 section = 'database'
400
401 if self._get_openstack_release() <= self.precise_havana:
402 # Section and directive for this config changed in icehouse
403 expected = {'sql_connection': db_uri, 'sql_idle_timeout': '3600'}
404 section = 'DEFAULT'
405
406 ret = u.validate_config_data(unit, conf, section, expected)
407 if ret:
408 message = "glance db config error: {}".format(ret)
409 amulet.raise_status(amulet.FAIL, msg=message)
410
411 def test_glance_registry_database_config(self):
412 '''Verify database config in glance-registry.conf and
413 compare with a db uri constructed from relation data.'''
414 unit = self.glance_sentry
415 conf = '/etc/glance/glance-registry.conf'
416 relation = self.mysql_sentry.relation('shared-db', 'glance:shared-db')
417 db_uri = "mysql://{}:{}@{}/{}".format('glance', relation['password'],
418 relation['db_host'], 'glance')
419 expected = {'connection': db_uri, 'sql_idle_timeout': '3600'}
420 section = 'database'
421
422 if self._get_openstack_release() <= self.precise_havana:
423 # Section and directive for this config changed in icehouse
424 expected = {'sql_connection': db_uri, 'sql_idle_timeout': '3600'}
425 section = 'DEFAULT'
426
427 ret = u.validate_config_data(unit, conf, section, expected)
428 if ret:
429 message = "glance db config error: {}".format(ret)
430 amulet.raise_status(amulet.FAIL, msg=message)
431
432 def test_glance_endpoint(self):
433 '''Verify the glance endpoint data.'''
434 endpoints = self.keystone.endpoints.list()
435 admin_port = internal_port = public_port = '9292'
436 expected = {'id': u.not_null,
437 'region': 'RegionOne',
438 'adminurl': u.valid_url,
439 'internalurl': u.valid_url,
440 'publicurl': u.valid_url,
441 'service_id': u.not_null}
442 ret = u.validate_endpoint_data(endpoints, admin_port, internal_port,
443 public_port, expected)
444
445 if ret:
446 amulet.raise_status(amulet.FAIL,
447 msg='glance endpoint: {}'.format(ret))
448
449 def test_keystone_endpoint(self):
450 '''Verify the keystone endpoint data.'''
451 endpoints = self.keystone.endpoints.list()
452 admin_port = '35357'
453 internal_port = public_port = '5000'
454 expected = {'id': u.not_null,
455 'region': 'RegionOne',
456 'adminurl': u.valid_url,
457 'internalurl': u.valid_url,
458 'publicurl': u.valid_url,
459 'service_id': u.not_null}
460 ret = u.validate_endpoint_data(endpoints, admin_port, internal_port,
461 public_port, expected)
462 if ret:
463 amulet.raise_status(amulet.FAIL,
464 msg='keystone endpoint: {}'.format(ret))
465
466 def _change_config(self):
467 if self._get_openstack_release() > self.precise_havana:
468 self.d.configure('glance', {'debug': 'True'})
469 else:
470 self.d.configure('glance', {'debug': 'False'})
471
472 def _restore_config(self):
473 if self._get_openstack_release() > self.precise_havana:
474 self.d.configure('glance', {'debug': 'False'})
475 else:
476 self.d.configure('glance', {'debug': 'True'})
477
478 def test_z_glance_restart_on_config_change(self):
479 '''Verify that glance is restarted when the config is changed.
480
481 Note(coreycb): The method name with the _z_ is a little odd
482 but it forces the test to run last. It just makes things
483 easier because restarting services requires re-authorization.
484 '''
485 if self._get_openstack_release() <= self.precise_havana:
486 # /!\ NOTE(beisner): Glance charm before Icehouse doesn't respond
487 # to attempted config changes via juju / juju set.
488 # https://bugs.launchpad.net/charms/+source/glance/+bug/1340307
489 u.log.error('NOTE(beisner): skipping glance restart on config ' +
490 'change check due to bug 1340307.')
491 return
492
493 # Make config change to trigger a service restart
494 self._change_config()
495
496 if not u.service_restarted(self.glance_sentry, 'glance-api',
497 '/etc/glance/glance-api.conf'):
498 self._restore_config()
499 message = "glance service didn't restart after config change"
500 amulet.raise_status(amulet.FAIL, msg=message)
501
502 if not u.service_restarted(self.glance_sentry, 'glance-registry',
503 '/etc/glance/glance-registry.conf',
504 sleep_time=0):
505 self._restore_config()
506 message = "glance service didn't restart after config change"
507 amulet.raise_status(amulet.FAIL, msg=message)
508
509 # Return to original config
510 self._restore_config()
511
512 def test_users(self):
513 '''Verify expected users.'''
514 user0 = {'name': 'glance',
515 'enabled': True,
516 'tenantId': u.not_null,
517 'id': u.not_null,
518 'email': 'juju@localhost'}
519 user1 = {'name': 'admin',
520 'enabled': True,
521 'tenantId': u.not_null,
522 'id': u.not_null,
523 'email': 'juju@localhost'}
524 expected = [user0, user1]
525 actual = self.keystone.users.list()
526
527 ret = u.validate_user_data(expected, actual)
528 if ret:
529 amulet.raise_status(amulet.FAIL, msg=ret)
530539
=== modified file 'tests/charmhelpers/contrib/amulet/utils.py'
--- tests/charmhelpers/contrib/amulet/utils.py 2015-06-19 15:08:48 +0000
+++ tests/charmhelpers/contrib/amulet/utils.py 2015-07-02 12:52:15 +0000
@@ -185,10 +185,23 @@
185 for k in expected.keys():185 for k in expected.keys():
186 if not config.has_option(section, k):186 if not config.has_option(section, k):
187 return "section [{}] is missing option {}".format(section, k)187 return "section [{}] is missing option {}".format(section, k)
188 if config.get(section, k) != expected[k]:188
189 return "section [{}] {}:{} != expected {}:{}".format(189 actual = config.get(section, k)
190 section, k, config.get(section, k), k, expected[k])190 v = expected[k]
191 return None191 if (isinstance(v, six.string_types) or
192 isinstance(v, bool) or
193 isinstance(v, six.integer_types)):
194 # handle explicit values
195 if actual != v:
196 return "section [{}] {}:{} != expected {}:{}".format(
197 section, k, actual, k, expected[k])
198 else:
199 # handle not_null, valid_ip boolean comparison methods, etc.
200 if v(actual):
201 return None
202 else:
203 return "section [{}] {}:{} != expected {}:{}".format(
204 section, k, actual, k, expected[k])
192205
193 def _validate_dict_data(self, expected, actual):206 def _validate_dict_data(self, expected, actual):
194 """Validate dictionary data.207 """Validate dictionary data.
@@ -406,3 +419,123 @@
406 """Convert a relative file path to a file URL."""419 """Convert a relative file path to a file URL."""
407 _abs_path = os.path.abspath(file_rel_path)420 _abs_path = os.path.abspath(file_rel_path)
408 return urlparse.urlparse(_abs_path, scheme='file').geturl()421 return urlparse.urlparse(_abs_path, scheme='file').geturl()
422
423 def check_commands_on_units(self, commands, sentry_units):
424 """Check that all commands in a list exit zero on all
425 sentry units in a list.
426
427 :param commands: list of bash commands
428 :param sentry_units: list of sentry unit pointers
429 :returns: None if successful; Failure message otherwise
430 """
431 self.log.debug('Checking exit codes for {} commands on {} '
432 'sentry units...'.format(len(commands),
433 len(sentry_units)))
434 for sentry_unit in sentry_units:
435 for cmd in commands:
436 output, code = sentry_unit.run(cmd)
437 if code == 0:
438 msg = ('{} `{}` returned {} '
439 '(OK)'.format(sentry_unit.info['unit_name'],
440 cmd, code))
441 self.log.debug(msg)
442 else:
443 msg = ('{} `{}` returned {} '
444 '{}'.format(sentry_unit.info['unit_name'],
445 cmd, code, output))
446 return msg
447 return None
448
449 def get_process_id_list(self, sentry_unit, process_name):
450 """Get a list of process ID(s) from a single sentry juju unit
451 for a single process name.
452
453 :param sentry_unit: Pointer to amulet sentry instance (juju unit)
454 :param process_name: Process name
455 :returns: List of process IDs
456 """
457 cmd = 'pidof {}'.format(process_name)
458 output, code = sentry_unit.run(cmd)
459 if code != 0:
460 msg = ('{} `{}` returned {} '
461 '{}'.format(sentry_unit.info['unit_name'],
462 cmd, code, output))
463 raise RuntimeError(msg)
464 return str(output).split()
465
466 def get_unit_process_ids(self, unit_processes):
467 """Construct a dict containing unit sentries, process names, and
468 process IDs."""
469 pid_dict = {}
470 for sentry_unit, process_list in unit_processes.iteritems():
471 pid_dict[sentry_unit] = {}
472 for process in process_list:
473 pids = self.get_process_id_list(sentry_unit, process)
474 pid_dict[sentry_unit].update({process: pids})
475 return pid_dict
476
477 def validate_unit_process_ids(self, expected, actual):
478 """Validate process id quantities for services on units."""
479 self.log.debug('Checking units for running processes...')
480 self.log.debug('Expected PIDs: {}'.format(expected))
481 self.log.debug('Actual PIDs: {}'.format(actual))
482
483 if len(actual) != len(expected):
484 msg = ('Unit count mismatch. expected, actual: {}, '
485 '{} '.format(len(expected), len(actual)))
486 return msg
487
488 for (e_sentry, e_proc_names) in expected.iteritems():
489 e_sentry_name = e_sentry.info['unit_name']
490 if e_sentry in actual.keys():
491 a_proc_names = actual[e_sentry]
492 else:
493 msg = ('Expected sentry ({}) not found in actual dict data.'
494 '{}'.format(e_sentry_name, e_sentry))
495 return msg
496
497 if len(e_proc_names.keys()) != len(a_proc_names.keys()):
498 msg = ('Process name count mismatch. expected, actual: {}, '
499 '{}'.format(len(expected), len(actual)))
500 return msg
501
502 for (e_proc_name, e_pids_length), (a_proc_name, a_pids) in \
503 zip(e_proc_names.items(), a_proc_names.items()):
504 if e_proc_name != a_proc_name:
505 msg = ('Process name mismatch. expected, actual: {}, '
506 '{}'.format(e_proc_name, a_proc_name))
507 return msg
508
509 a_pids_length = len(a_pids)
510 if e_pids_length != a_pids_length:
511 msg = ('PID count mismatch. {} ({}) expected, actual: {}, '
512 '{} ({})'.format(e_sentry_name,
513 e_proc_name,
514 e_pids_length,
515 a_pids_length,
516 a_pids))
517 return msg
518 else:
519 msg = ('PID check OK: {} {} {}: '
520 '{}'.format(e_sentry_name,
521 e_proc_name,
522 e_pids_length,
523 a_pids))
524 self.log.debug(msg)
525 return None
526
527 def validate_list_of_identical_dicts(self, list_of_dicts):
528 """Check that all dicts within a list are identical."""
529 hashes = []
530 for _dict in list_of_dicts:
531 hashes.append(hash(frozenset(_dict.items())))
532
533 self.log.debug('Hashes: {}'.format(hashes))
534 if len(set(hashes)) == 1:
535 msg = 'Dicts within list are identical'
536 self.log.debug(msg)
537 else:
538 msg = 'Dicts within list are not identical'
539 return msg
540
541 return None
409542
=== modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-19 15:08:48 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-07-02 12:52:15 +0000
@@ -79,9 +79,9 @@
79 services.append(this_service)79 services.append(this_service)
80 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',80 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
81 'ceph-osd', 'ceph-radosgw']81 'ceph-osd', 'ceph-radosgw']
82 # Openstack subordinate charms do not expose an origin option as that82 # Most OpenStack subordinate charms do not expose an origin option
83 # is controlled by the principle83 # as that is controlled by the principle.
84 ignore = ['neutron-openvswitch']84 ignore = ['cinder-ceph', 'hacluster', 'neutron-openvswitch']
8585
86 if self.openstack:86 if self.openstack:
87 for svc in services:87 for svc in services:
@@ -148,3 +148,35 @@
148 return os_origin.split('%s-' % self.series)[1].split('/')[0]148 return os_origin.split('%s-' % self.series)[1].split('/')[0]
149 else:149 else:
150 return releases[self.series]150 return releases[self.series]
151
152 def get_ceph_expected_pools(self, radosgw=False):
153 """Return a list of expected ceph pools based on Ubuntu-OpenStack
154 release and whether ceph radosgw is flagged as present or not."""
155
156 if self._get_openstack_release() >= self.trusty_kilo:
157 # Kilo or later
158 pools = [
159 'rbd',
160 'cinder',
161 'glance'
162 ]
163 else:
164 # Juno or earlier
165 pools = [
166 'data',
167 'metadata',
168 'rbd',
169 'cinder',
170 'glance'
171 ]
172
173 if radosgw:
174 pools.extend([
175 '.rgw.root',
176 '.rgw.control',
177 '.rgw',
178 '.rgw.gc',
179 '.users.uid'
180 ])
181
182 return pools
151183
=== modified file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
--- tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-06-19 15:08:48 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-07-02 12:52:15 +0000
@@ -14,16 +14,19 @@
14# You should have received a copy of the GNU Lesser General Public License14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1616
17import json
17import logging18import logging
18import os19import os
19import six20import six
20import time21import time
21import urllib22import urllib
2223
24import cinderclient.v1.client as cinder_client
23import glanceclient.v1.client as glance_client25import glanceclient.v1.client as glance_client
24import heatclient.v1.client as heat_client26import heatclient.v1.client as heat_client
25import keystoneclient.v2_0 as keystone_client27import keystoneclient.v2_0 as keystone_client
26import novaclient.v1_1.client as nova_client28import novaclient.v1_1.client as nova_client
29import swiftclient
2730
28from charmhelpers.contrib.amulet.utils import (31from charmhelpers.contrib.amulet.utils import (
29 AmuletUtils32 AmuletUtils
@@ -171,6 +174,15 @@
171 self.log.debug('Checking if tenant exists ({})...'.format(tenant))174 self.log.debug('Checking if tenant exists ({})...'.format(tenant))
172 return tenant in [t.name for t in keystone.tenants.list()]175 return tenant in [t.name for t in keystone.tenants.list()]
173176
177 def authenticate_cinder_admin(self, keystone_sentry, username,
178 password, tenant):
179 """Authenticates admin user with cinder."""
180 service_ip = \
181 keystone_sentry.relation('shared-db',
182 'mysql:shared-db')['private-address']
183 ept = "http://{}:5000/v2.0".format(service_ip.strip().decode('utf-8'))
184 return cinder_client.Client(username, password, tenant, ept)
185
174 def authenticate_keystone_admin(self, keystone_sentry, user, password,186 def authenticate_keystone_admin(self, keystone_sentry, user, password,
175 tenant):187 tenant):
176 """Authenticates admin user with the keystone admin endpoint."""188 """Authenticates admin user with the keystone admin endpoint."""
@@ -212,9 +224,29 @@
212 return nova_client.Client(username=user, api_key=password,224 return nova_client.Client(username=user, api_key=password,
213 project_id=tenant, auth_url=ep)225 project_id=tenant, auth_url=ep)
214226
227 def authenticate_swift_user(self, keystone, user, password, tenant):
228 """Authenticates a regular user with swift api."""
229 self.log.debug('Authenticating swift user ({})...'.format(user))
230 ep = keystone.service_catalog.url_for(service_type='identity',
231 endpoint_type='publicURL')
232 return swiftclient.Connection(authurl=ep,
233 user=user,
234 key=password,
235 tenant_name=tenant,
236 auth_version='2.0')
237
215 def create_cirros_image(self, glance, image_name):238 def create_cirros_image(self, glance, image_name):
216 """Download the latest cirros image and upload it to glance."""239 """Download the latest cirros image and upload it to glance,
217 self.log.debug('Creating glance image ({})...'.format(image_name))240 validate and return a resource pointer.
241
242 :param glance: pointer to authenticated glance connection
243 :param image_name: display name for new image
244 :returns: glance image pointer
245 """
246 self.log.debug('Creating glance cirros image '
247 '({})...'.format(image_name))
248
249 # Download cirros image
218 http_proxy = os.getenv('AMULET_HTTP_PROXY')250 http_proxy = os.getenv('AMULET_HTTP_PROXY')
219 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))251 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
220 if http_proxy:252 if http_proxy:
@@ -223,33 +255,51 @@
223 else:255 else:
224 opener = urllib.FancyURLopener()256 opener = urllib.FancyURLopener()
225257
226 f = opener.open("http://download.cirros-cloud.net/version/released")258 f = opener.open('http://download.cirros-cloud.net/version/released')
227 version = f.read().strip()259 version = f.read().strip()
228 cirros_img = "cirros-{}-x86_64-disk.img".format(version)260 cirros_img = 'cirros-{}-x86_64-disk.img'.format(version)
229 local_path = os.path.join('tests', cirros_img)261 local_path = os.path.join('tests', cirros_img)
230262
231 if not os.path.exists(local_path):263 if not os.path.exists(local_path):
232 cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",264 cirros_url = 'http://{}/{}/{}'.format('download.cirros-cloud.net',
233 version, cirros_img)265 version, cirros_img)
234 opener.retrieve(cirros_url, local_path)266 opener.retrieve(cirros_url, local_path)
235 f.close()267 f.close()
236268
269 # Create glance image
237 with open(local_path) as f:270 with open(local_path) as f:
238 image = glance.images.create(name=image_name, is_public=True,271 image = glance.images.create(name=image_name, is_public=True,
239 disk_format='qcow2',272 disk_format='qcow2',
240 container_format='bare', data=f)273 container_format='bare', data=f)
241 count = 1274
242 status = image.status275 # Wait for image to reach active status
243 while status != 'active' and count < 10:276 img_id = image.id
244 time.sleep(3)277 ret = self.resource_reaches_status(glance.images, img_id,
245 image = glance.images.get(image.id)278 expected_stat='active',
246 status = image.status279 msg='Image status wait')
247 self.log.debug('image status: {}'.format(status))280 if not ret:
248 count += 1281 msg = 'Glance image failed to reach expected state.'
249282 raise RuntimeError(msg)
250 if status != 'active':283
251 self.log.error('image creation timed out')284 # Re-validate new image
252 return None285 self.log.debug('Validating image attributes...')
286 val_img_name = glance.images.get(img_id).name
287 val_img_stat = glance.images.get(img_id).status
288 val_img_pub = glance.images.get(img_id).is_public
289 val_img_cfmt = glance.images.get(img_id).container_format
290 val_img_dfmt = glance.images.get(img_id).disk_format
291 msg_attr = ('Image attributes - name:{} public:{} id:{} stat:{} '
292 'container fmt:{} disk fmt:{}'.format(
293 val_img_name, val_img_pub, img_id,
294 val_img_stat, val_img_cfmt, val_img_dfmt))
295
296 if val_img_name == image_name and val_img_stat == 'active' \
297 and val_img_pub is True and val_img_cfmt == 'bare' \
298 and val_img_dfmt == 'qcow2':
299 self.log.debug(msg_attr)
300 else:
301 msg = ('Volume validation failed, {}'.format(msg_attr))
302 raise RuntimeError(msg)
253303
254 return image304 return image
255305
@@ -260,22 +310,7 @@
260 self.log.warn('/!\\ DEPRECATION WARNING: use '310 self.log.warn('/!\\ DEPRECATION WARNING: use '
261 'delete_resource instead of delete_image.')311 'delete_resource instead of delete_image.')
262 self.log.debug('Deleting glance image ({})...'.format(image))312 self.log.debug('Deleting glance image ({})...'.format(image))
263 num_before = len(list(glance.images.list()))313 return self.delete_resource(glance.images, image, msg='glance image')
264 glance.images.delete(image)
265
266 count = 1
267 num_after = len(list(glance.images.list()))
268 while num_after != (num_before - 1) and count < 10:
269 time.sleep(3)
270 num_after = len(list(glance.images.list()))
271 self.log.debug('number of images: {}'.format(num_after))
272 count += 1
273
274 if num_after != (num_before - 1):
275 self.log.error('image deletion timed out')
276 return False
277
278 return True
279314
280 def create_instance(self, nova, image_name, instance_name, flavor):315 def create_instance(self, nova, image_name, instance_name, flavor):
281 """Create the specified instance."""316 """Create the specified instance."""
@@ -308,22 +343,8 @@
308 self.log.warn('/!\\ DEPRECATION WARNING: use '343 self.log.warn('/!\\ DEPRECATION WARNING: use '
309 'delete_resource instead of delete_instance.')344 'delete_resource instead of delete_instance.')
310 self.log.debug('Deleting instance ({})...'.format(instance))345 self.log.debug('Deleting instance ({})...'.format(instance))
311 num_before = len(list(nova.servers.list()))346 return self.delete_resource(nova.servers, instance,
312 nova.servers.delete(instance)347 msg='nova instance')
313
314 count = 1
315 num_after = len(list(nova.servers.list()))
316 while num_after != (num_before - 1) and count < 10:
317 time.sleep(3)
318 num_after = len(list(nova.servers.list()))
319 self.log.debug('number of instances: {}'.format(num_after))
320 count += 1
321
322 if num_after != (num_before - 1):
323 self.log.error('instance deletion timed out')
324 return False
325
326 return True
327348
328 def create_or_get_keypair(self, nova, keypair_name="testkey"):349 def create_or_get_keypair(self, nova, keypair_name="testkey"):
329 """Create a new keypair, or return pointer if it already exists."""350 """Create a new keypair, or return pointer if it already exists."""
@@ -339,6 +360,84 @@
339 _keypair = nova.keypairs.create(name=keypair_name)360 _keypair = nova.keypairs.create(name=keypair_name)
340 return _keypair361 return _keypair
341362
363 def create_cinder_volume(self, cinder, vol_name="demo-vol", vol_size=1,
364 img_id=None, src_vol_id=None, snap_id=None):
365 """Create cinder volume, optionally from a glance image, or
366 optionally as a clone of an existing volume, or optionally
367 from a snapshot. Wait for the new volume status to reach
368 the expected status, validate and return a resource pointer.
369
370 :param vol_name: cinder volume display name
371 :param vol_size: size in gigabytes
372 :param img_id: optional glance image id
373 :param src_vol_id: optional source volume id to clone
374 :param snap_id: optional snapshot id to use
375 :returns: cinder volume pointer
376 """
377 # Handle parameter input
378 if img_id and not src_vol_id and not snap_id:
379 self.log.debug('Creating cinder volume from glance image '
380 '({})...'.format(img_id))
381 bootable = 'true'
382 elif src_vol_id and not img_id and not snap_id:
383 self.log.debug('Cloning cinder volume...')
384 bootable = cinder.volumes.get(src_vol_id).bootable
385 elif snap_id and not src_vol_id and not img_id:
386 self.log.debug('Creating cinder volume from snapshot...')
387 snap = cinder.volume_snapshots.find(id=snap_id)
388 vol_size = snap.size
389 snap_vol_id = cinder.volume_snapshots.get(snap_id).volume_id
390 bootable = cinder.volumes.get(snap_vol_id).bootable
391 elif not img_id and not src_vol_id and not snap_id:
392 self.log.debug('Creating cinder volume...')
393 bootable = 'false'
394 else:
395 msg = ('Invalid method use - name:{} size:{} img_id:{} '
396 'src_vol_id:{} snap_id:{}'.format(vol_name, vol_size,
397 img_id, src_vol_id,
398 snap_id))
399 raise RuntimeError(msg)
400
401 # Create new volume
402 try:
403 vol_new = cinder.volumes.create(display_name=vol_name,
404 imageRef=img_id,
405 size=vol_size,
406 source_volid=src_vol_id,
407 snapshot_id=snap_id)
408 vol_id = vol_new.id
409 except Exception as e:
410 msg = 'Failed to create volume: {}'.format(e)
411 raise RuntimeError(msg)
412
413 # Wait for volume to reach available status
414 ret = self.resource_reaches_status(cinder.volumes, vol_id,
415 expected_stat="available",
416 msg="Volume status wait")
417 if not ret:
418 msg = 'Cinder volume failed to reach expected state.'
419 raise RuntimeError(msg)
420
421 # Re-validate new volume
422 self.log.debug('Validating volume attributes...')
423 val_vol_name = cinder.volumes.get(vol_id).display_name
424 val_vol_boot = cinder.volumes.get(vol_id).bootable
425 val_vol_stat = cinder.volumes.get(vol_id).status
426 val_vol_size = cinder.volumes.get(vol_id).size
427 msg_attr = ('Volume attributes - name:{} id:{} stat:{} boot:'
428 '{} size:{}'.format(val_vol_name, vol_id,
429 val_vol_stat, val_vol_boot,
430 val_vol_size))
431
432 if val_vol_boot == bootable and val_vol_stat == 'available' \
433 and val_vol_name == vol_name and val_vol_size == vol_size:
434 self.log.debug(msg_attr)
435 else:
436 msg = ('Volume validation failed, {}'.format(msg_attr))
437 raise RuntimeError(msg)
438
439 return vol_new
440
342 def delete_resource(self, resource, resource_id,441 def delete_resource(self, resource, resource_id,
343 msg="resource", max_wait=120):442 msg="resource", max_wait=120):
344 """Delete one openstack resource, such as one instance, keypair,443 """Delete one openstack resource, such as one instance, keypair,
@@ -350,6 +449,8 @@
350 :param max_wait: maximum wait time in seconds449 :param max_wait: maximum wait time in seconds
351 :returns: True if successful, otherwise False450 :returns: True if successful, otherwise False
352 """451 """
452 self.log.debug('Deleting OpenStack resource '
453 '{} ({})'.format(resource_id, msg))
353 num_before = len(list(resource.list()))454 num_before = len(list(resource.list()))
354 resource.delete(resource_id)455 resource.delete(resource_id)
355456
@@ -411,3 +512,90 @@
411 self.log.debug('{} never reached expected status: '512 self.log.debug('{} never reached expected status: '
412 '{}'.format(resource_id, expected_stat))513 '{}'.format(resource_id, expected_stat))
413 return False514 return False
515
516 def get_ceph_osd_id_cmd(self, index):
517 """Produce a shell command that will return a ceph-osd id."""
518 cmd = ("`initctl list | grep 'ceph-osd ' | awk 'NR=={} {{ print $2 }}'"
519 " | grep -o '[0-9]*'`".format(index + 1))
520 return cmd
521
522 def get_ceph_pools(self, sentry_unit):
523 """Return a dict of ceph pools from a single ceph unit, with
524 pool name as keys, pool id as vals."""
525 pools = {}
526 cmd = 'sudo ceph osd lspools'
527 output, code = sentry_unit.run(cmd)
528 if code != 0:
529 msg = ('{} `{}` returned {} '
530 '{}'.format(sentry_unit.info['unit_name'],
531 cmd, code, output))
532 raise RuntimeError(msg)
533
534 # Example output: 0 data,1 metadata,2 rbd,3 cinder,4 glance,
535 for pool in str(output).split(','):
536 pool_id_name = pool.split(' ')
537 if len(pool_id_name) == 2:
538 pool_id = pool_id_name[0]
539 pool_name = pool_id_name[1]
540 pools[pool_name] = int(pool_id)
541
542 self.log.debug('Pools on {}: {}'.format(sentry_unit.info['unit_name'],
543 pools))
544 return pools
545
546 def get_ceph_df(self, sentry_unit):
547 """Return dict of ceph df json output, including ceph pool state.
548
549 :param sentry_unit: Pointer to amulet sentry instance (juju unit)
550 :returns: Dict of ceph df output
551 """
552 cmd = 'sudo ceph df --format=json'
553 output, code = sentry_unit.run(cmd)
554 if code != 0:
555 msg = ('{} `{}` returned {} '
556 '{}'.format(sentry_unit.info['unit_name'],
557 cmd, code, output))
558 raise RuntimeError(msg)
559 return json.loads(output)
560
561 def get_ceph_pool_sample(self, sentry_unit, pool_id=0):
562 """Take a sample of attributes of a ceph pool, returning ceph
563 pool name, object count and disk space used for the specified
564 pool ID number.
565
566 :param sentry_unit: Pointer to amulet sentry instance (juju unit)
567 :param pool_id: Ceph pool ID
568 :returns: List of pool name, object count, kb disk space used
569 """
570 df = self.get_ceph_df(sentry_unit)
571 pool_name = df['pools'][pool_id]['name']
572 obj_count = df['pools'][pool_id]['stats']['objects']
573 kb_used = df['pools'][pool_id]['stats']['kb_used']
574 self.log.debug('Ceph {} pool (ID {}): {} objects, '
575 '{} kb used'.format(pool_name,
576 pool_id,
577 obj_count,
578 kb_used))
579 return pool_name, obj_count, kb_used
580
581 def validate_ceph_pool_samples(self, samples, sample_type="resource pool"):
582 """Validate ceph pool samples taken over time, such as pool
583 object counts or pool kb used, before adding, after adding, and
584 after deleting items which affect those pool attributes. The
585 2nd element is expected to be greater than the 1st; 3rd is expected
586 to be less than the 2nd.
587
588 :param samples: List containing 3 data samples
589 :param sample_type: String for logging and usage context
590 :returns: None if successful, Failure message otherwise
591 """
592 original, created, deleted = range(3)
593 if samples[created] <= samples[original] or \
594 samples[deleted] >= samples[created]:
595 msg = ('Ceph {} samples ({}) '
596 'unexpected.'.format(sample_type, samples))
597 return msg
598 else:
599 self.log.debug('Ceph {} samples (OK): '
600 '{}'.format(sample_type, samples))
601 return None
414602
=== added file 'tests/tests.yaml'
--- tests/tests.yaml 1970-01-01 00:00:00 +0000
+++ tests/tests.yaml 2015-07-02 12:52:15 +0000
@@ -0,0 +1,18 @@
1bootstrap: true
2reset: true
3virtualenv: true
4makefile:
5 - lint
6 - test
7sources:
8 - ppa:juju/stable
9packages:
10 - amulet
11 - python-amulet
12 - python-cinderclient
13 - python-distro-info
14 - python-glanceclient
15 - python-heatclient
16 - python-keystoneclient
17 - python-novaclient
18 - python-swiftclient

Subscribers

People subscribed via source and target branches