Merge lp:~lezbar/charms/trusty/neutron-agents-midonet/trunk into lp:~celebdor/charms/trusty/neutron-agents-midonet/trunk

Proposed by Antoni Segura Puimedon
Status: Merged
Approved by: Antoni Segura Puimedon
Approved revision: 7
Merged at revision: 7
Proposed branch: lp:~lezbar/charms/trusty/neutron-agents-midonet/trunk
Merge into: lp:~celebdor/charms/trusty/neutron-agents-midonet/trunk
Diff against target: 1305 lines (+1235/-0)
13 files modified
tests/00-setup (+19/-0)
tests/010-basic-trusty-juno (+24/-0)
tests/011-basic-trusty-kilo (+24/-0)
tests/basic_deployment.py (+214/-0)
tests/charmhelpers/__init__.py (+38/-0)
tests/charmhelpers/contrib/__init__.py (+15/-0)
tests/charmhelpers/contrib/amulet/__init__.py (+15/-0)
tests/charmhelpers/contrib/amulet/deployment.py (+93/-0)
tests/charmhelpers/contrib/amulet/utils.py (+323/-0)
tests/charmhelpers/contrib/openstack/__init__.py (+15/-0)
tests/charmhelpers/contrib/openstack/amulet/__init__.py (+15/-0)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+146/-0)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+294/-0)
To merge this branch: bzr merge lp:~lezbar/charms/trusty/neutron-agents-midonet/trunk
Reviewer Review Type Date Requested Status
Antoni Segura Puimedon Approve
Review via email: mp+274842@code.launchpad.net

Description of the change

Addition of the amulet tests.

To post a comment you must log in.
Revision history for this message
Antoni Segura Puimedon (celebdor) wrote :

Looks good to me. Thanks Lucas.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'tests'
2=== added file 'tests/00-setup'
3--- tests/00-setup 1970-01-01 00:00:00 +0000
4+++ tests/00-setup 2015-10-19 03:23:14 +0000
5@@ -0,0 +1,19 @@
6+#!/bin/bash
7+#
8+# Copyright (c) 2015 Midokura SARL, All Rights Reserved.
9+#
10+# Licensed under the Apache License, Version 2.0 (the "License");
11+# you may not use this file except in compliance with the License.
12+# You may obtain a copy of the License at
13+#
14+# http://www.apache.org/licenses/LICENSE-2.0
15+#
16+# Unless required by applicable law or agreed to in writing, software
17+# distributed under the License is distributed on an "AS IS" BASIS,
18+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19+# See the License for the specific language governing permissions and
20+# limitations under the License.
21+
22+sudo add-apt-repository ppa:juju/stable -y
23+sudo apt-get update
24+sudo apt-get install amulet python3-requests -y
25
26=== added file 'tests/010-basic-trusty-juno'
27--- tests/010-basic-trusty-juno 1970-01-01 00:00:00 +0000
28+++ tests/010-basic-trusty-juno 2015-10-19 03:23:14 +0000
29@@ -0,0 +1,24 @@
30+#!/usr/bin/python
31+#
32+# Copyright (c) 2015 Midokura SARL, All Rights Reserved.
33+#
34+# Licensed under the Apache License, Version 2.0 (the "License");
35+# you may not use this file except in compliance with the License.
36+# You may obtain a copy of the License at
37+#
38+# http://www.apache.org/licenses/LICENSE-2.0
39+#
40+# Unless required by applicable law or agreed to in writing, software
41+# distributed under the License is distributed on an "AS IS" BASIS,
42+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
43+# See the License for the specific language governing permissions and
44+# limitations under the License.
45+
46+
47+from basic_deployment import MidonetBasicDeployment
48+
49+if __name__ == '__main__':
50+ deployment = MidonetBasicDeployment(ubuntu_series='trusty',
51+ openstack='cloud:trusty-juno',
52+ midonet_release='juno/midonet-2015.06')
53+ deployment.run_tests()
54
55=== added file 'tests/011-basic-trusty-kilo'
56--- tests/011-basic-trusty-kilo 1970-01-01 00:00:00 +0000
57+++ tests/011-basic-trusty-kilo 2015-10-19 03:23:14 +0000
58@@ -0,0 +1,24 @@
59+#!/usr/bin/python
60+#
61+# Copyright (c) 2015 Midokura SARL, All Rights Reserved.
62+#
63+# Licensed under the Apache License, Version 2.0 (the "License");
64+# you may not use this file except in compliance with the License.
65+# You may obtain a copy of the License at
66+#
67+# http://www.apache.org/licenses/LICENSE-2.0
68+#
69+# Unless required by applicable law or agreed to in writing, software
70+# distributed under the License is distributed on an "AS IS" BASIS,
71+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
72+# See the License for the specific language governing permissions and
73+# limitations under the License.
74+
75+
76+from basic_deployment import MidonetBasicDeployment
77+
78+if __name__ == '__main__':
79+ deployment = MidonetBasicDeployment(ubuntu_series='trusty',
80+ openstack='cloud:trusty-kilo',
81+ midonet_release='kilo/midonet-2015.06')
82+ deployment.run_tests()
83
84=== added file 'tests/basic_deployment.py'
85--- tests/basic_deployment.py 1970-01-01 00:00:00 +0000
86+++ tests/basic_deployment.py 2015-10-19 03:23:14 +0000
87@@ -0,0 +1,214 @@
88+#!/usr/bin/env python3
89+# vim: tabstop=4 shiftwidth=4 softtabstop=4 filetype=python
90+#
91+# Copyright (c) 2015 Midokura Europe SARL, All Rights Reserved.
92+# All Rights Reserved
93+#
94+# Licensed under the Apache License, Version 2.0 (the "License"); you may
95+# not use this file except in compliance with the License. You may obtain
96+# a copy of the License at
97+#
98+# http://www.apache.org/licenses/LICENSE-2.0
99+#
100+# Unless required by applicable law or agreed to in writing, software
101+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
102+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
103+# License for the specific language governing permissions and limitations
104+# under the License.
105+import amulet
106+
107+from charmhelpers.contrib.openstack.amulet.utils import (
108+ OpenStackAmuletUtils,
109+ DEBUG, # flake8: noqa
110+ ERROR
111+)
112+
113+# Use DEBUG to turn on debug logging
114+u = OpenStackAmuletUtils(DEBUG)
115+
116+SETUP_TIMEOUT = 9600
117+
118+
119+class MidonetBasicDeployment():
120+ def __init__(self, ubuntu_series, openstack=None, midonet_release=None):
121+ self.os_release = openstack
122+ self.series = ubuntu_series
123+ self.midonet_release = midonet_release
124+ self.d = amulet.Deployment(series=self.series)
125+ self._add_services()
126+ self._add_relations()
127+ self._configure_services()
128+ self._deploy()
129+ self._initialize_tests()
130+
131+ def _add_services(self):
132+ self.d.add('mysql', charm='cs:trusty/mysql', units=1,
133+ series=self.series)
134+ self.d.add('rabbitmq-server', charm='cs:trusty/rabbitmq-server',
135+ units=1, series=self.series)
136+ self.d.add('cassandra', charm='cassandra', units=1,
137+ branch='lp:charms/cassandra', series=self.series)
138+ self.d.add('keystone', charm='keystone', units=1, series=self.series,
139+ branch='lp:~celebdor/charms/trusty/keystone/trunk')
140+ self.d.add('nova-compute', charm='nova-compute', units=1,
141+ branch='lp:~celebdor/charms/trusty/nova-compute/trunk',
142+ series=self.series)
143+ self.d.add('neutron-api', charm='neutron-api', units=1,
144+ branch='lp:~celebdor/charms/trusty/neutron-api/trunk',
145+ series=self.series)
146+ self.d.add('nova-cloud-controller', charm='nova-cloud-controller',
147+ branch='lp:~celebdor/charms/trusty/nova-cloud-controller/trunk',
148+ units=1, series=self.series)
149+ self.d.add('zookeeper', charm='cs:trusty/zookeeper', units=1,
150+ series=self.series)
151+ self.d.add('midonet-api', charm='midonet-api', units=1,
152+ branch='lp:~celebdor/charms/trusty/midonet-api/trunk',
153+ series=self.series)
154+ self.d.add('midonet-agent', charm='midonet-agent', units=1,
155+ branch='lp:~celebdor/charms/trusty/midonet-agent/trunk',
156+ series=self.series)
157+ self.d.add('neutron-agents-midonet', charm='neutron-agents-midonet',
158+ branch='lp:~celebdor/charms/trusty/neutron-agents-midonet/trunk',
159+ series='trusty')
160+
161+ def _add_relations(self):
162+ self.d.relate('mysql:shared-db', 'keystone:shared-db')
163+ self.d.relate('mysql:shared-db', 'nova-cloud-controller:shared-db')
164+ self.d.relate('rabbitmq-server:amqp', 'nova-compute:amqp')
165+ self.d.relate('rabbitmq-server:amqp', 'nova-cloud-controller:amqp')
166+ self.d.relate('rabbitmq-server:amqp', 'neutron-api:amqp')
167+ self.d.relate('nova-cloud-controller:identity-service',
168+ 'keystone:identity-service')
169+
170+ self.d.relate('neutron-api:identity-service',
171+ 'keystone:identity-service')
172+ self.d.relate('nova-compute:cloud-compute',
173+ 'nova-cloud-controller:cloud-compute')
174+ self.d.relate('neutron-api:neutron-api',
175+ 'nova-cloud-controller:neutron-api')
176+
177+ self.d.relate('keystone:identity-service', 'midonet-api:keystone')
178+ self.d.relate('zookeeper:zookeeper', 'midonet-api:zookeeper')
179+ self.d.relate('midonet-agent:host', 'midonet-api:host')
180+ self.d.relate('neutron-api:midonet', 'midonet-api:midonet-api')
181+
182+ self.d.relate('neutron-agents-midonet:neutron_agents',
183+ 'nova-cloud-controller:quantum-network-service')
184+
185+ self.d.relate('neutron-agents-midonet:neutron-plugin-api',
186+ 'neutron-api:neutron-plugin-api')
187+
188+ self.d.relate('midonet-agent:neutron-plugin',
189+ 'nova-compute:neutron-plugin')
190+
191+ self.d.relate('midonet-agent:host', 'neutron-api:midonet-host')
192+ self.d.relate('midonet-agent:cassandra', 'cassandra:database')
193+ self.d.relate('midonet-agent:zookeeper', 'zookeeper:zookeeper')
194+
195+ def _configure_services(self):
196+ self.d.configure('keystone', {
197+ 'enable-pki': 'false',
198+ 'openstack-origin': self.os_release})
199+
200+ self.d.configure('cassandra', {
201+ 'allow-single-node': True,
202+ 'cluster-name': 'midonet',
203+ 'apt-repo-key': '7E41C00F85BFC1706C4FFFB3350200F2B999A372',
204+ 'apt-repo-spec':
205+ 'deb http://debian.datastax.com/community 2.0 main',
206+ 'extra_packages': 'openjdk-7-jre-headless dsc20'})
207+
208+ self.d.configure('mysql', {'max-connections': 2000})
209+
210+ self.d.configure('nova-compute', {
211+ 'openstack-origin': self.os_release,
212+ 'virt-type': 'qemu',
213+ 'flat-interface': 'eth0',
214+ 'manage-neutron-plugin-legacy-mode': 'false'})
215+
216+ self.d.configure('nova-cloud-controller', {
217+ 'openstack-origin': self.os_release,
218+ 'network-manager': 'Neutron',
219+ 'shared_secret': 'secret'})
220+
221+ self.d.configure('neutron-api', {
222+ 'neutron-plugin': 'midonet',
223+ 'neutron-security-groups': 'True',
224+ 'neutron-external-network': 'Public_Network',
225+ 'l2-population': 'False',
226+ 'openstack-origin': self.os_release,
227+ 'midonet-release': self.midonet_release})
228+
229+ self.d.configure('midonet-api',
230+ {'midonet-release': self.midonet_release})
231+
232+ self.d.configure('neutron-agents-midonet',
233+ {'shared_secret': 'secret'})
234+
235+ self.d.configure('midonet-agent',
236+ {'midonet-release': self.midonet_release})
237+
238+ def _deploy(self):
239+ try:
240+ self.d.setup(timeout=SETUP_TIMEOUT)
241+ self.d.sentry.wait(timeout=SETUP_TIMEOUT)
242+ except amulet.helpers.TimeoutError:
243+ amulet.raise_status(amulet.SKIP,
244+ msg="Environment wasn't stood up in time")
245+
246+ def _initialize_tests(self):
247+ self.zookeeper_sentry = self.d.sentry.unit['zookeeper/0']
248+ self.keystone_sentry = self.d.sentry.unit['keystone/0']
249+ self.cassandra_sentry = self.d.sentry.unit['cassandra/0']
250+ self.nova_compute_sentry = self.d.sentry.unit['nova-compute/0']
251+ self.mn_api_sentry = self.d.sentry.unit['midonet-api/0']
252+ self.mn_agent_sentry = self.d.sentry.unit['midonet-agent/0']
253+ self.agents_mn_sentry = self.d.sentry.unit['neutron-agents-midonet/0']
254+
255+ self.keystone_api_relation = self.keystone_sentry.relation(
256+ 'identity-service', 'midonet-api:keystone')
257+ self.zookeeper_api_relation = self.zookeeper_sentry.relation(
258+ 'zookeeper', 'midonet-api:zookeeper')
259+ self.zk_host_relation = self.zookeeper_sentry.relation(
260+ 'zookeeper', 'midonet-agent:zookeeper')
261+ self.cs_host_relation = self.cassandra_sentry.relation(
262+ 'database', 'midonet-agent:cassandra')
263+ self.api_host_relation = self.mn_api_sentry.relation(
264+ 'host', 'midonet-agent:host')
265+
266+ def run_tests(self):
267+ for test in dir(self):
268+ if test.startswith('test_'):
269+ getattr(self, test)()
270+
271+ def test_neutron_agents_mn(self):
272+ u.log.debug('Checking if the key has been correctly fetched...')
273+ apt_key_out, apt_key_exit = self.mn_agent_sentry.run(
274+ 'apt-key list | grep Midokura')
275+ if 'Midokura' not in apt_key_out:
276+ message = ("error, Midokura repository key is not installed")
277+ amulet.raise_status(amulet.FAIL, msg=message)
278+
279+ u.log.debug('Checking if midonet-plugin package has been installed...')
280+ dpkg_plugin_out, dpkg_exit = self.agents_mn_sentry.run(
281+ ('dpkg -l | grep python-neutron-plugin-midonet'))
282+ if 'python-neutron-plugin-midonet' not in dpkg_plugin_out:
283+ message = (
284+ "error: python-neutron-plugin-midonet package is not installed")
285+ amulet.raise_status(amulet.FAIL, msg=message)
286+
287+ u.log.debug('Checking neutron-dhcp-agent...')
288+ dhcp_agent_out, dhcp_agent_exit = self.agents_mn_sentry.run(
289+ 'service neutron-dhcp-agent status')
290+ if 'running' not in dhcp_agent_out:
291+ message = (
292+ "error, neutron-dhcp-agent not running: %s" % dhcp_agent_out)
293+ amulet.raise_status(amulet.FAIL, msg=message)
294+
295+ u.log.debug('Checking neutron-metadata-agent...')
296+ meta_agent_out, meta_agent_exit = self.agents_mn_sentry.run(
297+ 'service neutron-metadata-agent status')
298+ if 'running' not in meta_agent_out:
299+ message = (
300+ "error, neutron-metadata-agent not running: %s" % meta_agent_out)
301+ amulet.raise_status(amulet.FAIL, msg=message)
302
303=== added directory 'tests/charmhelpers'
304=== added file 'tests/charmhelpers/__init__.py'
305--- tests/charmhelpers/__init__.py 1970-01-01 00:00:00 +0000
306+++ tests/charmhelpers/__init__.py 2015-10-19 03:23:14 +0000
307@@ -0,0 +1,38 @@
308+# Copyright 2014-2015 Canonical Limited.
309+#
310+# This file is part of charm-helpers.
311+#
312+# charm-helpers is free software: you can redistribute it and/or modify
313+# it under the terms of the GNU Lesser General Public License version 3 as
314+# published by the Free Software Foundation.
315+#
316+# charm-helpers is distributed in the hope that it will be useful,
317+# but WITHOUT ANY WARRANTY; without even the implied warranty of
318+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
319+# GNU Lesser General Public License for more details.
320+#
321+# You should have received a copy of the GNU Lesser General Public License
322+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
323+
324+# Bootstrap charm-helpers, installing its dependencies if necessary using
325+# only standard libraries.
326+import subprocess
327+import sys
328+
329+try:
330+ import six # flake8: noqa
331+except ImportError:
332+ if sys.version_info.major == 2:
333+ subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
334+ else:
335+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
336+ import six # flake8: noqa
337+
338+try:
339+ import yaml # flake8: noqa
340+except ImportError:
341+ if sys.version_info.major == 2:
342+ subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
343+ else:
344+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
345+ import yaml # flake8: noqa
346
347=== added directory 'tests/charmhelpers/contrib'
348=== added file 'tests/charmhelpers/contrib/__init__.py'
349--- tests/charmhelpers/contrib/__init__.py 1970-01-01 00:00:00 +0000
350+++ tests/charmhelpers/contrib/__init__.py 2015-10-19 03:23:14 +0000
351@@ -0,0 +1,15 @@
352+# Copyright 2014-2015 Canonical Limited.
353+#
354+# This file is part of charm-helpers.
355+#
356+# charm-helpers is free software: you can redistribute it and/or modify
357+# it under the terms of the GNU Lesser General Public License version 3 as
358+# published by the Free Software Foundation.
359+#
360+# charm-helpers is distributed in the hope that it will be useful,
361+# but WITHOUT ANY WARRANTY; without even the implied warranty of
362+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
363+# GNU Lesser General Public License for more details.
364+#
365+# You should have received a copy of the GNU Lesser General Public License
366+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
367
368=== added directory 'tests/charmhelpers/contrib/amulet'
369=== added file 'tests/charmhelpers/contrib/amulet/__init__.py'
370--- tests/charmhelpers/contrib/amulet/__init__.py 1970-01-01 00:00:00 +0000
371+++ tests/charmhelpers/contrib/amulet/__init__.py 2015-10-19 03:23:14 +0000
372@@ -0,0 +1,15 @@
373+# Copyright 2014-2015 Canonical Limited.
374+#
375+# This file is part of charm-helpers.
376+#
377+# charm-helpers is free software: you can redistribute it and/or modify
378+# it under the terms of the GNU Lesser General Public License version 3 as
379+# published by the Free Software Foundation.
380+#
381+# charm-helpers is distributed in the hope that it will be useful,
382+# but WITHOUT ANY WARRANTY; without even the implied warranty of
383+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
384+# GNU Lesser General Public License for more details.
385+#
386+# You should have received a copy of the GNU Lesser General Public License
387+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
388
389=== added file 'tests/charmhelpers/contrib/amulet/deployment.py'
390--- tests/charmhelpers/contrib/amulet/deployment.py 1970-01-01 00:00:00 +0000
391+++ tests/charmhelpers/contrib/amulet/deployment.py 2015-10-19 03:23:14 +0000
392@@ -0,0 +1,93 @@
393+# Copyright 2014-2015 Canonical Limited.
394+#
395+# This file is part of charm-helpers.
396+#
397+# charm-helpers is free software: you can redistribute it and/or modify
398+# it under the terms of the GNU Lesser General Public License version 3 as
399+# published by the Free Software Foundation.
400+#
401+# charm-helpers is distributed in the hope that it will be useful,
402+# but WITHOUT ANY WARRANTY; without even the implied warranty of
403+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
404+# GNU Lesser General Public License for more details.
405+#
406+# You should have received a copy of the GNU Lesser General Public License
407+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
408+
409+import amulet
410+import os
411+import six
412+
413+
414+class AmuletDeployment(object):
415+ """Amulet deployment.
416+
417+ This class provides generic Amulet deployment and test runner
418+ methods.
419+ """
420+
421+ def __init__(self, series=None):
422+ """Initialize the deployment environment."""
423+ self.series = None
424+
425+ if series:
426+ self.series = series
427+ self.d = amulet.Deployment(series=self.series)
428+ else:
429+ self.d = amulet.Deployment()
430+
431+ def _add_services(self, this_service, other_services):
432+ """Add services.
433+
434+ Add services to the deployment where this_service is the local charm
435+ that we're testing and other_services are the other services that
436+ are being used in the local amulet tests.
437+ """
438+ if this_service['name'] != os.path.basename(os.getcwd()):
439+ s = this_service['name']
440+ msg = "The charm's root directory name needs to be {}".format(s)
441+ amulet.raise_status(amulet.FAIL, msg=msg)
442+
443+ if 'units' not in this_service:
444+ this_service['units'] = 1
445+
446+ self.d.add(this_service['name'], units=this_service['units'])
447+
448+ for svc in other_services:
449+ if 'location' in svc:
450+ branch_location = svc['location']
451+ elif self.series:
452+ branch_location = 'cs:{}/{}'.format(self.series, svc['name']),
453+ else:
454+ branch_location = None
455+
456+ if 'units' not in svc:
457+ svc['units'] = 1
458+
459+ self.d.add(svc['name'], charm=branch_location, units=svc['units'])
460+
461+ def _add_relations(self, relations):
462+ """Add all of the relations for the services."""
463+ for k, v in six.iteritems(relations):
464+ self.d.relate(k, v)
465+
466+ def _configure_services(self, configs):
467+ """Configure all of the services."""
468+ for service, config in six.iteritems(configs):
469+ self.d.configure(service, config)
470+
471+ def _deploy(self):
472+ """Deploy environment and wait for all hooks to finish executing."""
473+ try:
474+ self.d.setup(timeout=900)
475+ self.d.sentry.wait(timeout=900)
476+ except amulet.helpers.TimeoutError:
477+ amulet.raise_status(amulet.FAIL, msg="Deployment timed out")
478+ except Exception:
479+ raise
480+
481+ def run_tests(self):
482+ """Run all of the methods that are prefixed with 'test_'."""
483+ for test in dir(self):
484+ if test.startswith('test_'):
485+ getattr(self, test)()
486
487=== added file 'tests/charmhelpers/contrib/amulet/utils.py'
488--- tests/charmhelpers/contrib/amulet/utils.py 1970-01-01 00:00:00 +0000
489+++ tests/charmhelpers/contrib/amulet/utils.py 2015-10-19 03:23:14 +0000
490@@ -0,0 +1,323 @@
491+# Copyright 2014-2015 Canonical Limited.
492+#
493+# This file is part of charm-helpers.
494+#
495+# charm-helpers is free software: you can redistribute it and/or modify
496+# it under the terms of the GNU Lesser General Public License version 3 as
497+# published by the Free Software Foundation.
498+#
499+# charm-helpers is distributed in the hope that it will be useful,
500+# but WITHOUT ANY WARRANTY; without even the implied warranty of
501+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
502+# GNU Lesser General Public License for more details.
503+#
504+# You should have received a copy of the GNU Lesser General Public License
505+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
506+
507+import ConfigParser
508+import io
509+import logging
510+import re
511+import sys
512+import time
513+
514+import six
515+
516+
517+class AmuletUtils(object):
518+ """Amulet utilities.
519+
520+ This class provides common utility functions that are used by Amulet
521+ tests.
522+ """
523+
524+ def __init__(self, log_level=logging.ERROR):
525+ self.log = self.get_logger(level=log_level)
526+
527+ def get_logger(self, name="amulet-logger", level=logging.DEBUG):
528+ """Get a logger object that will log to stdout."""
529+ log = logging
530+ logger = log.getLogger(name)
531+ fmt = log.Formatter("%(asctime)s %(funcName)s "
532+ "%(levelname)s: %(message)s")
533+
534+ handler = log.StreamHandler(stream=sys.stdout)
535+ handler.setLevel(level)
536+ handler.setFormatter(fmt)
537+
538+ logger.addHandler(handler)
539+ logger.setLevel(level)
540+
541+ return logger
542+
543+ def valid_ip(self, ip):
544+ if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip):
545+ return True
546+ else:
547+ return False
548+
549+ def valid_url(self, url):
550+ p = re.compile(
551+ r'^(?:http|ftp)s?://'
552+ r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # noqa
553+ r'localhost|'
554+ r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
555+ r'(?::\d+)?'
556+ r'(?:/?|[/?]\S+)$',
557+ re.IGNORECASE)
558+ if p.match(url):
559+ return True
560+ else:
561+ return False
562+
563+ def validate_services(self, commands):
564+ """Validate services.
565+
566+ Verify the specified services are running on the corresponding
567+ service units.
568+ """
569+ for k, v in six.iteritems(commands):
570+ for cmd in v:
571+ output, code = k.run(cmd)
572+ self.log.debug('{} `{}` returned '
573+ '{}'.format(k.info['unit_name'],
574+ cmd, code))
575+ if code != 0:
576+ return "command `{}` returned {}".format(cmd, str(code))
577+ return None
578+
579+ def _get_config(self, unit, filename):
580+ """Get a ConfigParser object for parsing a unit's config file."""
581+ file_contents = unit.file_contents(filename)
582+
583+ # NOTE(beisner): by default, ConfigParser does not handle options
584+ # with no value, such as the flags used in the mysql my.cnf file.
585+ # https://bugs.python.org/issue7005
586+ config = ConfigParser.ConfigParser(allow_no_value=True)
587+ config.readfp(io.StringIO(file_contents))
588+ return config
589+
590+ def validate_config_data(self, sentry_unit, config_file, section,
591+ expected):
592+ """Validate config file data.
593+
594+ Verify that the specified section of the config file contains
595+ the expected option key:value pairs.
596+ """
597+ config = self._get_config(sentry_unit, config_file)
598+
599+ if section != 'DEFAULT' and not config.has_section(section):
600+ return "section [{}] does not exist".format(section)
601+
602+ for k in expected.keys():
603+ if not config.has_option(section, k):
604+ return "section [{}] is missing option {}".format(section, k)
605+ if config.get(section, k) != expected[k]:
606+ return "section [{}] {}:{} != expected {}:{}".format(
607+ section, k, config.get(section, k), k, expected[k])
608+ return None
609+
610+ def _validate_dict_data(self, expected, actual):
611+ """Validate dictionary data.
612+
613+ Compare expected dictionary data vs actual dictionary data.
614+ The values in the 'expected' dictionary can be strings, bools, ints,
615+ longs, or can be a function that evaluate a variable and returns a
616+ bool.
617+ """
618+ self.log.debug('actual: {}'.format(repr(actual)))
619+ self.log.debug('expected: {}'.format(repr(expected)))
620+
621+ for k, v in six.iteritems(expected):
622+ if k in actual:
623+ if (isinstance(v, six.string_types) or
624+ isinstance(v, bool) or
625+ isinstance(v, six.integer_types)):
626+ if v != actual[k]:
627+ return "{}:{}".format(k, actual[k])
628+ elif not v(actual[k]):
629+ return "{}:{}".format(k, actual[k])
630+ else:
631+ return "key '{}' does not exist".format(k)
632+ return None
633+
634+ def validate_relation_data(self, sentry_unit, relation, expected):
635+ """Validate actual relation data based on expected relation data."""
636+ actual = sentry_unit.relation(relation[0], relation[1])
637+ return self._validate_dict_data(expected, actual)
638+
639+ def _validate_list_data(self, expected, actual):
640+ """Compare expected list vs actual list data."""
641+ for e in expected:
642+ if e not in actual:
643+ return "expected item {} not found in actual list".format(e)
644+ return None
645+
646+ def not_null(self, string):
647+ if string is not None:
648+ return True
649+ else:
650+ return False
651+
652+ def _get_file_mtime(self, sentry_unit, filename):
653+ """Get last modification time of file."""
654+ return sentry_unit.file_stat(filename)['mtime']
655+
656+ def _get_dir_mtime(self, sentry_unit, directory):
657+ """Get last modification time of directory."""
658+ return sentry_unit.directory_stat(directory)['mtime']
659+
660+ def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False):
661+ """Get process' start time.
662+
663+ Determine start time of the process based on the last modification
664+ time of the /proc/pid directory. If pgrep_full is True, the process
665+ name is matched against the full command line.
666+ """
667+ if pgrep_full:
668+ cmd = 'pgrep -o -f {}'.format(service)
669+ else:
670+ cmd = 'pgrep -o {}'.format(service)
671+ cmd = cmd + ' | grep -v pgrep || exit 0'
672+ cmd_out = sentry_unit.run(cmd)
673+ self.log.debug('CMDout: ' + str(cmd_out))
674+ if cmd_out[0]:
675+ self.log.debug('Pid for %s %s' % (service, str(cmd_out[0])))
676+ proc_dir = '/proc/{}'.format(cmd_out[0].strip())
677+ return self._get_dir_mtime(sentry_unit, proc_dir)
678+
679+ def service_restarted(self, sentry_unit, service, filename,
680+ pgrep_full=False, sleep_time=20):
681+ """Check if service was restarted.
682+
683+ Compare a service's start time vs a file's last modification time
684+ (such as a config file for that service) to determine if the service
685+ has been restarted.
686+ """
687+ time.sleep(sleep_time)
688+ if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
689+ self._get_file_mtime(sentry_unit, filename)):
690+ return True
691+ else:
692+ return False
693+
694+ def service_restarted_since(self, sentry_unit, mtime, service,
695+ pgrep_full=False, sleep_time=20,
696+ retry_count=2):
697+ """Check if service was been started after a given time.
698+
699+ Args:
700+ sentry_unit (sentry): The sentry unit to check for the service on
701+ mtime (float): The epoch time to check against
702+ service (string): service name to look for in process table
703+ pgrep_full (boolean): Use full command line search mode with pgrep
704+ sleep_time (int): Seconds to sleep before looking for process
705+ retry_count (int): If service is not found, how many times to retry
706+
707+ Returns:
708+ bool: True if service found and its start time it newer than mtime,
709+ False if service is older than mtime or if service was
710+ not found.
711+ """
712+ self.log.debug('Checking %s restarted since %s' % (service, mtime))
713+ time.sleep(sleep_time)
714+ proc_start_time = self._get_proc_start_time(sentry_unit, service,
715+ pgrep_full)
716+ while retry_count > 0 and not proc_start_time:
717+ self.log.debug('No pid file found for service %s, will retry %i '
718+ 'more times' % (service, retry_count))
719+ time.sleep(30)
720+ proc_start_time = self._get_proc_start_time(sentry_unit, service,
721+ pgrep_full)
722+ retry_count = retry_count - 1
723+
724+ if not proc_start_time:
725+ self.log.warn('No proc start time found, assuming service did '
726+ 'not start')
727+ return False
728+ if proc_start_time >= mtime:
729+ self.log.debug('proc start time is newer than provided mtime'
730+ '(%s >= %s)' % (proc_start_time, mtime))
731+ return True
732+ else:
733+ self.log.warn('proc start time (%s) is older than provided mtime '
734+ '(%s), service did not restart' % (proc_start_time,
735+ mtime))
736+ return False
737+
738+ def config_updated_since(self, sentry_unit, filename, mtime,
739+ sleep_time=20):
740+ """Check if file was modified after a given time.
741+
742+ Args:
743+ sentry_unit (sentry): The sentry unit to check the file mtime on
744+ filename (string): The file to check mtime of
745+ mtime (float): The epoch time to check against
746+ sleep_time (int): Seconds to sleep before looking for process
747+
748+ Returns:
749+ bool: True if file was modified more recently than mtime, False if
750+ file was modified before mtime,
751+ """
752+ self.log.debug('Checking %s updated since %s' % (filename, mtime))
753+ time.sleep(sleep_time)
754+ file_mtime = self._get_file_mtime(sentry_unit, filename)
755+ if file_mtime >= mtime:
756+ self.log.debug('File mtime is newer than provided mtime '
757+ '(%s >= %s)' % (file_mtime, mtime))
758+ return True
759+ else:
760+ self.log.warn('File mtime %s is older than provided mtime %s'
761+ % (file_mtime, mtime))
762+ return False
763+
764+ def validate_service_config_changed(self, sentry_unit, mtime, service,
765+ filename, pgrep_full=False,
766+ sleep_time=20, retry_count=2):
767+ """Check service and file were updated after mtime
768+
769+ Args:
770+ sentry_unit (sentry): The sentry unit to check for the service on
771+ mtime (float): The epoch time to check against
772+ service (string): service name to look for in process table
773+ filename (string): The file to check mtime of
774+ pgrep_full (boolean): Use full command line search mode with pgrep
775+ sleep_time (int): Seconds to sleep before looking for process
776+ retry_count (int): If service is not found, how many times to retry
777+
778+ Typical Usage:
779+ u = OpenStackAmuletUtils(ERROR)
780+ ...
781+ mtime = u.get_sentry_time(self.cinder_sentry)
782+ self.d.configure('cinder', {'verbose': 'True', 'debug': 'True'})
783+ if not u.validate_service_config_changed(self.cinder_sentry,
784+ mtime,
785+ 'cinder-api',
786+ '/etc/cinder/cinder.conf')
787+ amulet.raise_status(amulet.FAIL, msg='update failed')
788+ Returns:
789+ bool: True if both service and file where updated/restarted after
790+ mtime, False if service is older than mtime or if service was
791+ not found or if filename was modified before mtime.
792+ """
793+ self.log.debug('Checking %s restarted since %s' % (service, mtime))
794+ time.sleep(sleep_time)
795+ service_restart = self.service_restarted_since(sentry_unit, mtime,
796+ service,
797+ pgrep_full=pgrep_full,
798+ sleep_time=0,
799+ retry_count=retry_count)
800+ config_update = self.config_updated_since(sentry_unit, filename, mtime,
801+ sleep_time=0)
802+ return service_restart and config_update
803+
804+ def get_sentry_time(self, sentry_unit):
805+ """Return current epoch time on a sentry"""
806+ cmd = "date +'%s'"
807+ return float(sentry_unit.run(cmd)[0])
808+
809+ def relation_error(self, name, data):
810+ return 'unexpected relation data in {} - {}'.format(name, data)
811+
812+ def endpoint_error(self, name, data):
813+ return 'unexpected endpoint data in {} - {}'.format(name, data)
814
815=== added directory 'tests/charmhelpers/contrib/openstack'
816=== added file 'tests/charmhelpers/contrib/openstack/__init__.py'
817--- tests/charmhelpers/contrib/openstack/__init__.py 1970-01-01 00:00:00 +0000
818+++ tests/charmhelpers/contrib/openstack/__init__.py 2015-10-19 03:23:14 +0000
819@@ -0,0 +1,15 @@
820+# Copyright 2014-2015 Canonical Limited.
821+#
822+# This file is part of charm-helpers.
823+#
824+# charm-helpers is free software: you can redistribute it and/or modify
825+# it under the terms of the GNU Lesser General Public License version 3 as
826+# published by the Free Software Foundation.
827+#
828+# charm-helpers is distributed in the hope that it will be useful,
829+# but WITHOUT ANY WARRANTY; without even the implied warranty of
830+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
831+# GNU Lesser General Public License for more details.
832+#
833+# You should have received a copy of the GNU Lesser General Public License
834+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
835
836=== added directory 'tests/charmhelpers/contrib/openstack/amulet'
837=== added file 'tests/charmhelpers/contrib/openstack/amulet/__init__.py'
838--- tests/charmhelpers/contrib/openstack/amulet/__init__.py 1970-01-01 00:00:00 +0000
839+++ tests/charmhelpers/contrib/openstack/amulet/__init__.py 2015-10-19 03:23:14 +0000
840@@ -0,0 +1,15 @@
841+# Copyright 2014-2015 Canonical Limited.
842+#
843+# This file is part of charm-helpers.
844+#
845+# charm-helpers is free software: you can redistribute it and/or modify
846+# it under the terms of the GNU Lesser General Public License version 3 as
847+# published by the Free Software Foundation.
848+#
849+# charm-helpers is distributed in the hope that it will be useful,
850+# but WITHOUT ANY WARRANTY; without even the implied warranty of
851+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
852+# GNU Lesser General Public License for more details.
853+#
854+# You should have received a copy of the GNU Lesser General Public License
855+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
856
857=== added file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
858--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 1970-01-01 00:00:00 +0000
859+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-10-19 03:23:14 +0000
860@@ -0,0 +1,146 @@
861+# Copyright 2014-2015 Canonical Limited.
862+#
863+# This file is part of charm-helpers.
864+#
865+# charm-helpers is free software: you can redistribute it and/or modify
866+# it under the terms of the GNU Lesser General Public License version 3 as
867+# published by the Free Software Foundation.
868+#
869+# charm-helpers is distributed in the hope that it will be useful,
870+# but WITHOUT ANY WARRANTY; without even the implied warranty of
871+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
872+# GNU Lesser General Public License for more details.
873+#
874+# You should have received a copy of the GNU Lesser General Public License
875+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
876+
877+import six
878+from collections import OrderedDict
879+from charmhelpers.contrib.amulet.deployment import (
880+ AmuletDeployment
881+)
882+
883+
884+class OpenStackAmuletDeployment(AmuletDeployment):
885+ """OpenStack amulet deployment.
886+
887+ This class inherits from AmuletDeployment and has additional support
888+ that is specifically for use by OpenStack charms.
889+ """
890+
891+ def __init__(self, series=None, openstack=None, source=None, stable=True):
892+ """Initialize the deployment environment."""
893+ super(OpenStackAmuletDeployment, self).__init__(series)
894+ self.openstack = openstack
895+ self.source = source
896+ self.stable = stable
897+ # Note(coreycb): this needs to be changed when new next branches come
898+ # out.
899+ self.current_next = "trusty"
900+
901+ def _determine_branch_locations(self, other_services):
902+ """Determine the branch locations for the other services.
903+
904+ Determine if the local branch being tested is derived from its
905+ stable or next (dev) branch, and based on this, use the corresonding
906+ stable or next branches for the other_services."""
907+ base_charms = ['mysql', 'mongodb']
908+
909+ if self.series in ['precise', 'trusty']:
910+ base_series = self.series
911+ else:
912+ base_series = self.current_next
913+
914+ if self.stable:
915+ for svc in other_services:
916+ temp = 'lp:charms/{}/{}'
917+ svc['location'] = temp.format(base_series,
918+ svc['name'])
919+ else:
920+ for svc in other_services:
921+ if svc['name'] in base_charms:
922+ temp = 'lp:charms/{}/{}'
923+ svc['location'] = temp.format(base_series,
924+ svc['name'])
925+ else:
926+ temp = 'lp:~openstack-charmers/charms/{}/{}/next'
927+ svc['location'] = temp.format(self.current_next,
928+ svc['name'])
929+ return other_services
930+
931+ def _add_services(self, this_service, other_services):
932+ """Add services to the deployment and set openstack-origin/source."""
933+ other_services = self._determine_branch_locations(other_services)
934+
935+ super(OpenStackAmuletDeployment, self)._add_services(this_service,
936+ other_services)
937+
938+ services = other_services
939+ services.append(this_service)
940+ use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
941+ 'ceph-osd', 'ceph-radosgw']
942+ # Openstack subordinate charms do not expose an origin option as that
943+ # is controlled by the principle
944+ ignore = ['neutron-openvswitch']
945+
946+ if self.openstack:
947+ for svc in services:
948+ if svc['name'] not in use_source + ignore:
949+ config = {'openstack-origin': self.openstack}
950+ self.d.configure(svc['name'], config)
951+
952+ if self.source:
953+ for svc in services:
954+ if svc['name'] in use_source and svc['name'] not in ignore:
955+ config = {'source': self.source}
956+ self.d.configure(svc['name'], config)
957+
958+ def _configure_services(self, configs):
959+ """Configure all of the services."""
960+ for service, config in six.iteritems(configs):
961+ self.d.configure(service, config)
962+
963+ def _get_openstack_release(self):
964+ """Get openstack release.
965+
966+ Return an integer representing the enum value of the openstack
967+ release.
968+ """
969+ # Must be ordered by OpenStack release (not by Ubuntu release):
970+ (self.precise_essex, self.precise_folsom, self.precise_grizzly,
971+ self.precise_havana, self.precise_icehouse,
972+ self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
973+ self.trusty_kilo, self.vivid_kilo) = range(10)
974+
975+ releases = {
976+ ('precise', None): self.precise_essex,
977+ ('precise', 'cloud:precise-folsom'): self.precise_folsom,
978+ ('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
979+ ('precise', 'cloud:precise-havana'): self.precise_havana,
980+ ('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
981+ ('trusty', None): self.trusty_icehouse,
982+ ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
983+ ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
984+ ('utopic', None): self.utopic_juno,
985+ ('vivid', None): self.vivid_kilo}
986+ return releases[(self.series, self.openstack)]
987+
988+ def _get_openstack_release_string(self):
989+ """Get openstack release string.
990+
991+ Return a string representing the openstack release.
992+ """
993+ releases = OrderedDict([
994+ ('precise', 'essex'),
995+ ('quantal', 'folsom'),
996+ ('raring', 'grizzly'),
997+ ('saucy', 'havana'),
998+ ('trusty', 'icehouse'),
999+ ('utopic', 'juno'),
1000+ ('vivid', 'kilo'),
1001+ ])
1002+ if self.openstack:
1003+ os_origin = self.openstack.split(':')[1]
1004+ return os_origin.split('%s-' % self.series)[1].split('/')[0]
1005+ else:
1006+ return releases[self.series]
1007
1008=== added file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
1009--- tests/charmhelpers/contrib/openstack/amulet/utils.py 1970-01-01 00:00:00 +0000
1010+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-10-19 03:23:14 +0000
1011@@ -0,0 +1,294 @@
1012+# Copyright 2014-2015 Canonical Limited.
1013+#
1014+# This file is part of charm-helpers.
1015+#
1016+# charm-helpers is free software: you can redistribute it and/or modify
1017+# it under the terms of the GNU Lesser General Public License version 3 as
1018+# published by the Free Software Foundation.
1019+#
1020+# charm-helpers is distributed in the hope that it will be useful,
1021+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1022+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1023+# GNU Lesser General Public License for more details.
1024+#
1025+# You should have received a copy of the GNU Lesser General Public License
1026+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1027+
1028+import logging
1029+import os
1030+import time
1031+import urllib
1032+
1033+import glanceclient.v1.client as glance_client
1034+import keystoneclient.v2_0 as keystone_client
1035+import novaclient.v1_1.client as nova_client
1036+
1037+import six
1038+
1039+from charmhelpers.contrib.amulet.utils import (
1040+ AmuletUtils
1041+)
1042+
1043+DEBUG = logging.DEBUG
1044+ERROR = logging.ERROR
1045+
1046+
1047+class OpenStackAmuletUtils(AmuletUtils):
1048+ """OpenStack amulet utilities.
1049+
1050+ This class inherits from AmuletUtils and has additional support
1051+ that is specifically for use by OpenStack charms.
1052+ """
1053+
1054+ def __init__(self, log_level=ERROR):
1055+ """Initialize the deployment environment."""
1056+ super(OpenStackAmuletUtils, self).__init__(log_level)
1057+
1058+ def validate_endpoint_data(self, endpoints, admin_port, internal_port,
1059+ public_port, expected):
1060+ """Validate endpoint data.
1061+
1062+ Validate actual endpoint data vs expected endpoint data. The ports
1063+ are used to find the matching endpoint.
1064+ """
1065+ found = False
1066+ for ep in endpoints:
1067+ self.log.debug('endpoint: {}'.format(repr(ep)))
1068+ if (admin_port in ep.adminurl and
1069+ internal_port in ep.internalurl and
1070+ public_port in ep.publicurl):
1071+ found = True
1072+ actual = {'id': ep.id,
1073+ 'region': ep.region,
1074+ 'adminurl': ep.adminurl,
1075+ 'internalurl': ep.internalurl,
1076+ 'publicurl': ep.publicurl,
1077+ 'service_id': ep.service_id}
1078+ ret = self._validate_dict_data(expected, actual)
1079+ if ret:
1080+ return 'unexpected endpoint data - {}'.format(ret)
1081+
1082+ if not found:
1083+ return 'endpoint not found'
1084+
1085+ def validate_svc_catalog_endpoint_data(self, expected, actual):
1086+ """Validate service catalog endpoint data.
1087+
1088+ Validate a list of actual service catalog endpoints vs a list of
1089+ expected service catalog endpoints.
1090+ """
1091+ self.log.debug('actual: {}'.format(repr(actual)))
1092+ for k, v in six.iteritems(expected):
1093+ if k in actual:
1094+ ret = self._validate_dict_data(expected[k][0], actual[k][0])
1095+ if ret:
1096+ return self.endpoint_error(k, ret)
1097+ else:
1098+ return "endpoint {} does not exist".format(k)
1099+ return ret
1100+
1101+ def validate_tenant_data(self, expected, actual):
1102+ """Validate tenant data.
1103+
1104+ Validate a list of actual tenant data vs list of expected tenant
1105+ data.
1106+ """
1107+ self.log.debug('actual: {}'.format(repr(actual)))
1108+ for e in expected:
1109+ found = False
1110+ for act in actual:
1111+ a = {'enabled': act.enabled, 'description': act.description,
1112+ 'name': act.name, 'id': act.id}
1113+ if e['name'] == a['name']:
1114+ found = True
1115+ ret = self._validate_dict_data(e, a)
1116+ if ret:
1117+ return "unexpected tenant data - {}".format(ret)
1118+ if not found:
1119+ return "tenant {} does not exist".format(e['name'])
1120+ return ret
1121+
1122+ def validate_role_data(self, expected, actual):
1123+ """Validate role data.
1124+
1125+ Validate a list of actual role data vs a list of expected role
1126+ data.
1127+ """
1128+ self.log.debug('actual: {}'.format(repr(actual)))
1129+ for e in expected:
1130+ found = False
1131+ for act in actual:
1132+ a = {'name': act.name, 'id': act.id}
1133+ if e['name'] == a['name']:
1134+ found = True
1135+ ret = self._validate_dict_data(e, a)
1136+ if ret:
1137+ return "unexpected role data - {}".format(ret)
1138+ if not found:
1139+ return "role {} does not exist".format(e['name'])
1140+ return ret
1141+
1142+ def validate_user_data(self, expected, actual):
1143+ """Validate user data.
1144+
1145+ Validate a list of actual user data vs a list of expected user
1146+ data.
1147+ """
1148+ self.log.debug('actual: {}'.format(repr(actual)))
1149+ for e in expected:
1150+ found = False
1151+ for act in actual:
1152+ a = {'enabled': act.enabled, 'name': act.name,
1153+ 'email': act.email, 'tenantId': act.tenantId,
1154+ 'id': act.id}
1155+ if e['name'] == a['name']:
1156+ found = True
1157+ ret = self._validate_dict_data(e, a)
1158+ if ret:
1159+ return "unexpected user data - {}".format(ret)
1160+ if not found:
1161+ return "user {} does not exist".format(e['name'])
1162+ return ret
1163+
1164+ def validate_flavor_data(self, expected, actual):
1165+ """Validate flavor data.
1166+
1167+ Validate a list of actual flavors vs a list of expected flavors.
1168+ """
1169+ self.log.debug('actual: {}'.format(repr(actual)))
1170+ act = [a.name for a in actual]
1171+ return self._validate_list_data(expected, act)
1172+
1173+ def tenant_exists(self, keystone, tenant):
1174+ """Return True if tenant exists."""
1175+ return tenant in [t.name for t in keystone.tenants.list()]
1176+
1177+ def authenticate_keystone_admin(self, keystone_sentry, user, password,
1178+ tenant):
1179+ """Authenticates admin user with the keystone admin endpoint."""
1180+ unit = keystone_sentry
1181+ service_ip = unit.relation('shared-db',
1182+ 'mysql:shared-db')['private-address']
1183+ ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
1184+ return keystone_client.Client(username=user, password=password,
1185+ tenant_name=tenant, auth_url=ep)
1186+
1187+ def authenticate_keystone_user(self, keystone, user, password, tenant):
1188+ """Authenticates a regular user with the keystone public endpoint."""
1189+ ep = keystone.service_catalog.url_for(service_type='identity',
1190+ endpoint_type='publicURL')
1191+ return keystone_client.Client(username=user, password=password,
1192+ tenant_name=tenant, auth_url=ep)
1193+
1194+ def authenticate_glance_admin(self, keystone):
1195+ """Authenticates admin user with glance."""
1196+ ep = keystone.service_catalog.url_for(service_type='image',
1197+ endpoint_type='adminURL')
1198+ return glance_client.Client(ep, token=keystone.auth_token)
1199+
1200+ def authenticate_nova_user(self, keystone, user, password, tenant):
1201+ """Authenticates a regular user with nova-api."""
1202+ ep = keystone.service_catalog.url_for(service_type='identity',
1203+ endpoint_type='publicURL')
1204+ return nova_client.Client(username=user, api_key=password,
1205+ project_id=tenant, auth_url=ep)
1206+
1207+ def create_cirros_image(self, glance, image_name):
1208+ """Download the latest cirros image and upload it to glance."""
1209+ http_proxy = os.getenv('AMULET_HTTP_PROXY')
1210+ self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
1211+ if http_proxy:
1212+ proxies = {'http': http_proxy}
1213+ opener = urllib.FancyURLopener(proxies)
1214+ else:
1215+ opener = urllib.FancyURLopener()
1216+
1217+ f = opener.open("http://download.cirros-cloud.net/version/released")
1218+ version = f.read().strip()
1219+ cirros_img = "cirros-{}-x86_64-disk.img".format(version)
1220+ local_path = os.path.join('tests', cirros_img)
1221+
1222+ if not os.path.exists(local_path):
1223+ cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
1224+ version, cirros_img)
1225+ opener.retrieve(cirros_url, local_path)
1226+ f.close()
1227+
1228+ with open(local_path) as f:
1229+ image = glance.images.create(name=image_name, is_public=True,
1230+ disk_format='qcow2',
1231+ container_format='bare', data=f)
1232+ count = 1
1233+ status = image.status
1234+ while status != 'active' and count < 10:
1235+ time.sleep(3)
1236+ image = glance.images.get(image.id)
1237+ status = image.status
1238+ self.log.debug('image status: {}'.format(status))
1239+ count += 1
1240+
1241+ if status != 'active':
1242+ self.log.error('image creation timed out')
1243+ return None
1244+
1245+ return image
1246+
1247+ def delete_image(self, glance, image):
1248+ """Delete the specified image."""
1249+ num_before = len(list(glance.images.list()))
1250+ glance.images.delete(image)
1251+
1252+ count = 1
1253+ num_after = len(list(glance.images.list()))
1254+ while num_after != (num_before - 1) and count < 10:
1255+ time.sleep(3)
1256+ num_after = len(list(glance.images.list()))
1257+ self.log.debug('number of images: {}'.format(num_after))
1258+ count += 1
1259+
1260+ if num_after != (num_before - 1):
1261+ self.log.error('image deletion timed out')
1262+ return False
1263+
1264+ return True
1265+
1266+ def create_instance(self, nova, image_name, instance_name, flavor):
1267+ """Create the specified instance."""
1268+ image = nova.images.find(name=image_name)
1269+ flavor = nova.flavors.find(name=flavor)
1270+ instance = nova.servers.create(name=instance_name, image=image,
1271+ flavor=flavor)
1272+
1273+ count = 1
1274+ status = instance.status
1275+ while status != 'ACTIVE' and count < 60:
1276+ time.sleep(3)
1277+ instance = nova.servers.get(instance.id)
1278+ status = instance.status
1279+ self.log.debug('instance status: {}'.format(status))
1280+ count += 1
1281+
1282+ if status != 'ACTIVE':
1283+ self.log.error('instance creation timed out')
1284+ return None
1285+
1286+ return instance
1287+
1288+ def delete_instance(self, nova, instance):
1289+ """Delete the specified instance."""
1290+ num_before = len(list(nova.servers.list()))
1291+ nova.servers.delete(instance)
1292+
1293+ count = 1
1294+ num_after = len(list(nova.servers.list()))
1295+ while num_after != (num_before - 1) and count < 10:
1296+ time.sleep(3)
1297+ num_after = len(list(nova.servers.list()))
1298+ self.log.debug('number of instances: {}'.format(num_after))
1299+ count += 1
1300+
1301+ if num_after != (num_before - 1):
1302+ self.log.error('instance deletion timed out')
1303+ return False
1304+
1305+ return True

Subscribers

People subscribed via source and target branches