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
=== added directory 'tests'
=== added file 'tests/00-setup'
--- tests/00-setup 1970-01-01 00:00:00 +0000
+++ tests/00-setup 2015-10-19 03:23:14 +0000
@@ -0,0 +1,19 @@
1#!/bin/bash
2#
3# Copyright (c) 2015 Midokura SARL, All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17sudo add-apt-repository ppa:juju/stable -y
18sudo apt-get update
19sudo apt-get install amulet python3-requests -y
020
=== added file 'tests/010-basic-trusty-juno'
--- tests/010-basic-trusty-juno 1970-01-01 00:00:00 +0000
+++ tests/010-basic-trusty-juno 2015-10-19 03:23:14 +0000
@@ -0,0 +1,24 @@
1#!/usr/bin/python
2#
3# Copyright (c) 2015 Midokura SARL, All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17
18from basic_deployment import MidonetBasicDeployment
19
20if __name__ == '__main__':
21 deployment = MidonetBasicDeployment(ubuntu_series='trusty',
22 openstack='cloud:trusty-juno',
23 midonet_release='juno/midonet-2015.06')
24 deployment.run_tests()
025
=== added file 'tests/011-basic-trusty-kilo'
--- tests/011-basic-trusty-kilo 1970-01-01 00:00:00 +0000
+++ tests/011-basic-trusty-kilo 2015-10-19 03:23:14 +0000
@@ -0,0 +1,24 @@
1#!/usr/bin/python
2#
3# Copyright (c) 2015 Midokura SARL, All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17
18from basic_deployment import MidonetBasicDeployment
19
20if __name__ == '__main__':
21 deployment = MidonetBasicDeployment(ubuntu_series='trusty',
22 openstack='cloud:trusty-kilo',
23 midonet_release='kilo/midonet-2015.06')
24 deployment.run_tests()
025
=== added file 'tests/basic_deployment.py'
--- tests/basic_deployment.py 1970-01-01 00:00:00 +0000
+++ tests/basic_deployment.py 2015-10-19 03:23:14 +0000
@@ -0,0 +1,214 @@
1#!/usr/bin/env python3
2# vim: tabstop=4 shiftwidth=4 softtabstop=4 filetype=python
3#
4# Copyright (c) 2015 Midokura Europe SARL, All Rights Reserved.
5# All Rights Reserved
6#
7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8# not use this file except in compliance with the License. You may obtain
9# a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16# License for the specific language governing permissions and limitations
17# under the License.
18import amulet
19
20from charmhelpers.contrib.openstack.amulet.utils import (
21 OpenStackAmuletUtils,
22 DEBUG, # flake8: noqa
23 ERROR
24)
25
26# Use DEBUG to turn on debug logging
27u = OpenStackAmuletUtils(DEBUG)
28
29SETUP_TIMEOUT = 9600
30
31
32class MidonetBasicDeployment():
33 def __init__(self, ubuntu_series, openstack=None, midonet_release=None):
34 self.os_release = openstack
35 self.series = ubuntu_series
36 self.midonet_release = midonet_release
37 self.d = amulet.Deployment(series=self.series)
38 self._add_services()
39 self._add_relations()
40 self._configure_services()
41 self._deploy()
42 self._initialize_tests()
43
44 def _add_services(self):
45 self.d.add('mysql', charm='cs:trusty/mysql', units=1,
46 series=self.series)
47 self.d.add('rabbitmq-server', charm='cs:trusty/rabbitmq-server',
48 units=1, series=self.series)
49 self.d.add('cassandra', charm='cassandra', units=1,
50 branch='lp:charms/cassandra', series=self.series)
51 self.d.add('keystone', charm='keystone', units=1, series=self.series,
52 branch='lp:~celebdor/charms/trusty/keystone/trunk')
53 self.d.add('nova-compute', charm='nova-compute', units=1,
54 branch='lp:~celebdor/charms/trusty/nova-compute/trunk',
55 series=self.series)
56 self.d.add('neutron-api', charm='neutron-api', units=1,
57 branch='lp:~celebdor/charms/trusty/neutron-api/trunk',
58 series=self.series)
59 self.d.add('nova-cloud-controller', charm='nova-cloud-controller',
60 branch='lp:~celebdor/charms/trusty/nova-cloud-controller/trunk',
61 units=1, series=self.series)
62 self.d.add('zookeeper', charm='cs:trusty/zookeeper', units=1,
63 series=self.series)
64 self.d.add('midonet-api', charm='midonet-api', units=1,
65 branch='lp:~celebdor/charms/trusty/midonet-api/trunk',
66 series=self.series)
67 self.d.add('midonet-agent', charm='midonet-agent', units=1,
68 branch='lp:~celebdor/charms/trusty/midonet-agent/trunk',
69 series=self.series)
70 self.d.add('neutron-agents-midonet', charm='neutron-agents-midonet',
71 branch='lp:~celebdor/charms/trusty/neutron-agents-midonet/trunk',
72 series='trusty')
73
74 def _add_relations(self):
75 self.d.relate('mysql:shared-db', 'keystone:shared-db')
76 self.d.relate('mysql:shared-db', 'nova-cloud-controller:shared-db')
77 self.d.relate('rabbitmq-server:amqp', 'nova-compute:amqp')
78 self.d.relate('rabbitmq-server:amqp', 'nova-cloud-controller:amqp')
79 self.d.relate('rabbitmq-server:amqp', 'neutron-api:amqp')
80 self.d.relate('nova-cloud-controller:identity-service',
81 'keystone:identity-service')
82
83 self.d.relate('neutron-api:identity-service',
84 'keystone:identity-service')
85 self.d.relate('nova-compute:cloud-compute',
86 'nova-cloud-controller:cloud-compute')
87 self.d.relate('neutron-api:neutron-api',
88 'nova-cloud-controller:neutron-api')
89
90 self.d.relate('keystone:identity-service', 'midonet-api:keystone')
91 self.d.relate('zookeeper:zookeeper', 'midonet-api:zookeeper')
92 self.d.relate('midonet-agent:host', 'midonet-api:host')
93 self.d.relate('neutron-api:midonet', 'midonet-api:midonet-api')
94
95 self.d.relate('neutron-agents-midonet:neutron_agents',
96 'nova-cloud-controller:quantum-network-service')
97
98 self.d.relate('neutron-agents-midonet:neutron-plugin-api',
99 'neutron-api:neutron-plugin-api')
100
101 self.d.relate('midonet-agent:neutron-plugin',
102 'nova-compute:neutron-plugin')
103
104 self.d.relate('midonet-agent:host', 'neutron-api:midonet-host')
105 self.d.relate('midonet-agent:cassandra', 'cassandra:database')
106 self.d.relate('midonet-agent:zookeeper', 'zookeeper:zookeeper')
107
108 def _configure_services(self):
109 self.d.configure('keystone', {
110 'enable-pki': 'false',
111 'openstack-origin': self.os_release})
112
113 self.d.configure('cassandra', {
114 'allow-single-node': True,
115 'cluster-name': 'midonet',
116 'apt-repo-key': '7E41C00F85BFC1706C4FFFB3350200F2B999A372',
117 'apt-repo-spec':
118 'deb http://debian.datastax.com/community 2.0 main',
119 'extra_packages': 'openjdk-7-jre-headless dsc20'})
120
121 self.d.configure('mysql', {'max-connections': 2000})
122
123 self.d.configure('nova-compute', {
124 'openstack-origin': self.os_release,
125 'virt-type': 'qemu',
126 'flat-interface': 'eth0',
127 'manage-neutron-plugin-legacy-mode': 'false'})
128
129 self.d.configure('nova-cloud-controller', {
130 'openstack-origin': self.os_release,
131 'network-manager': 'Neutron',
132 'shared_secret': 'secret'})
133
134 self.d.configure('neutron-api', {
135 'neutron-plugin': 'midonet',
136 'neutron-security-groups': 'True',
137 'neutron-external-network': 'Public_Network',
138 'l2-population': 'False',
139 'openstack-origin': self.os_release,
140 'midonet-release': self.midonet_release})
141
142 self.d.configure('midonet-api',
143 {'midonet-release': self.midonet_release})
144
145 self.d.configure('neutron-agents-midonet',
146 {'shared_secret': 'secret'})
147
148 self.d.configure('midonet-agent',
149 {'midonet-release': self.midonet_release})
150
151 def _deploy(self):
152 try:
153 self.d.setup(timeout=SETUP_TIMEOUT)
154 self.d.sentry.wait(timeout=SETUP_TIMEOUT)
155 except amulet.helpers.TimeoutError:
156 amulet.raise_status(amulet.SKIP,
157 msg="Environment wasn't stood up in time")
158
159 def _initialize_tests(self):
160 self.zookeeper_sentry = self.d.sentry.unit['zookeeper/0']
161 self.keystone_sentry = self.d.sentry.unit['keystone/0']
162 self.cassandra_sentry = self.d.sentry.unit['cassandra/0']
163 self.nova_compute_sentry = self.d.sentry.unit['nova-compute/0']
164 self.mn_api_sentry = self.d.sentry.unit['midonet-api/0']
165 self.mn_agent_sentry = self.d.sentry.unit['midonet-agent/0']
166 self.agents_mn_sentry = self.d.sentry.unit['neutron-agents-midonet/0']
167
168 self.keystone_api_relation = self.keystone_sentry.relation(
169 'identity-service', 'midonet-api:keystone')
170 self.zookeeper_api_relation = self.zookeeper_sentry.relation(
171 'zookeeper', 'midonet-api:zookeeper')
172 self.zk_host_relation = self.zookeeper_sentry.relation(
173 'zookeeper', 'midonet-agent:zookeeper')
174 self.cs_host_relation = self.cassandra_sentry.relation(
175 'database', 'midonet-agent:cassandra')
176 self.api_host_relation = self.mn_api_sentry.relation(
177 'host', 'midonet-agent:host')
178
179 def run_tests(self):
180 for test in dir(self):
181 if test.startswith('test_'):
182 getattr(self, test)()
183
184 def test_neutron_agents_mn(self):
185 u.log.debug('Checking if the key has been correctly fetched...')
186 apt_key_out, apt_key_exit = self.mn_agent_sentry.run(
187 'apt-key list | grep Midokura')
188 if 'Midokura' not in apt_key_out:
189 message = ("error, Midokura repository key is not installed")
190 amulet.raise_status(amulet.FAIL, msg=message)
191
192 u.log.debug('Checking if midonet-plugin package has been installed...')
193 dpkg_plugin_out, dpkg_exit = self.agents_mn_sentry.run(
194 ('dpkg -l | grep python-neutron-plugin-midonet'))
195 if 'python-neutron-plugin-midonet' not in dpkg_plugin_out:
196 message = (
197 "error: python-neutron-plugin-midonet package is not installed")
198 amulet.raise_status(amulet.FAIL, msg=message)
199
200 u.log.debug('Checking neutron-dhcp-agent...')
201 dhcp_agent_out, dhcp_agent_exit = self.agents_mn_sentry.run(
202 'service neutron-dhcp-agent status')
203 if 'running' not in dhcp_agent_out:
204 message = (
205 "error, neutron-dhcp-agent not running: %s" % dhcp_agent_out)
206 amulet.raise_status(amulet.FAIL, msg=message)
207
208 u.log.debug('Checking neutron-metadata-agent...')
209 meta_agent_out, meta_agent_exit = self.agents_mn_sentry.run(
210 'service neutron-metadata-agent status')
211 if 'running' not in meta_agent_out:
212 message = (
213 "error, neutron-metadata-agent not running: %s" % meta_agent_out)
214 amulet.raise_status(amulet.FAIL, msg=message)
0215
=== added directory 'tests/charmhelpers'
=== added file 'tests/charmhelpers/__init__.py'
--- tests/charmhelpers/__init__.py 1970-01-01 00:00:00 +0000
+++ tests/charmhelpers/__init__.py 2015-10-19 03:23:14 +0000
@@ -0,0 +1,38 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# 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/>.
16
17# Bootstrap charm-helpers, installing its dependencies if necessary using
18# only standard libraries.
19import subprocess
20import sys
21
22try:
23 import six # flake8: noqa
24except ImportError:
25 if sys.version_info.major == 2:
26 subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
27 else:
28 subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
29 import six # flake8: noqa
30
31try:
32 import yaml # flake8: noqa
33except ImportError:
34 if sys.version_info.major == 2:
35 subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
36 else:
37 subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
38 import yaml # flake8: noqa
039
=== added directory 'tests/charmhelpers/contrib'
=== added file 'tests/charmhelpers/contrib/__init__.py'
--- tests/charmhelpers/contrib/__init__.py 1970-01-01 00:00:00 +0000
+++ tests/charmhelpers/contrib/__init__.py 2015-10-19 03:23:14 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# 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/>.
016
=== added directory 'tests/charmhelpers/contrib/amulet'
=== added file 'tests/charmhelpers/contrib/amulet/__init__.py'
--- tests/charmhelpers/contrib/amulet/__init__.py 1970-01-01 00:00:00 +0000
+++ tests/charmhelpers/contrib/amulet/__init__.py 2015-10-19 03:23:14 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# 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/>.
016
=== added file 'tests/charmhelpers/contrib/amulet/deployment.py'
--- tests/charmhelpers/contrib/amulet/deployment.py 1970-01-01 00:00:00 +0000
+++ tests/charmhelpers/contrib/amulet/deployment.py 2015-10-19 03:23:14 +0000
@@ -0,0 +1,93 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# 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/>.
16
17import amulet
18import os
19import six
20
21
22class AmuletDeployment(object):
23 """Amulet deployment.
24
25 This class provides generic Amulet deployment and test runner
26 methods.
27 """
28
29 def __init__(self, series=None):
30 """Initialize the deployment environment."""
31 self.series = None
32
33 if series:
34 self.series = series
35 self.d = amulet.Deployment(series=self.series)
36 else:
37 self.d = amulet.Deployment()
38
39 def _add_services(self, this_service, other_services):
40 """Add services.
41
42 Add services to the deployment where this_service is the local charm
43 that we're testing and other_services are the other services that
44 are being used in the local amulet tests.
45 """
46 if this_service['name'] != os.path.basename(os.getcwd()):
47 s = this_service['name']
48 msg = "The charm's root directory name needs to be {}".format(s)
49 amulet.raise_status(amulet.FAIL, msg=msg)
50
51 if 'units' not in this_service:
52 this_service['units'] = 1
53
54 self.d.add(this_service['name'], units=this_service['units'])
55
56 for svc in other_services:
57 if 'location' in svc:
58 branch_location = svc['location']
59 elif self.series:
60 branch_location = 'cs:{}/{}'.format(self.series, svc['name']),
61 else:
62 branch_location = None
63
64 if 'units' not in svc:
65 svc['units'] = 1
66
67 self.d.add(svc['name'], charm=branch_location, units=svc['units'])
68
69 def _add_relations(self, relations):
70 """Add all of the relations for the services."""
71 for k, v in six.iteritems(relations):
72 self.d.relate(k, v)
73
74 def _configure_services(self, configs):
75 """Configure all of the services."""
76 for service, config in six.iteritems(configs):
77 self.d.configure(service, config)
78
79 def _deploy(self):
80 """Deploy environment and wait for all hooks to finish executing."""
81 try:
82 self.d.setup(timeout=900)
83 self.d.sentry.wait(timeout=900)
84 except amulet.helpers.TimeoutError:
85 amulet.raise_status(amulet.FAIL, msg="Deployment timed out")
86 except Exception:
87 raise
88
89 def run_tests(self):
90 """Run all of the methods that are prefixed with 'test_'."""
91 for test in dir(self):
92 if test.startswith('test_'):
93 getattr(self, test)()
094
=== added file 'tests/charmhelpers/contrib/amulet/utils.py'
--- tests/charmhelpers/contrib/amulet/utils.py 1970-01-01 00:00:00 +0000
+++ tests/charmhelpers/contrib/amulet/utils.py 2015-10-19 03:23:14 +0000
@@ -0,0 +1,323 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# 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/>.
16
17import ConfigParser
18import io
19import logging
20import re
21import sys
22import time
23
24import six
25
26
27class AmuletUtils(object):
28 """Amulet utilities.
29
30 This class provides common utility functions that are used by Amulet
31 tests.
32 """
33
34 def __init__(self, log_level=logging.ERROR):
35 self.log = self.get_logger(level=log_level)
36
37 def get_logger(self, name="amulet-logger", level=logging.DEBUG):
38 """Get a logger object that will log to stdout."""
39 log = logging
40 logger = log.getLogger(name)
41 fmt = log.Formatter("%(asctime)s %(funcName)s "
42 "%(levelname)s: %(message)s")
43
44 handler = log.StreamHandler(stream=sys.stdout)
45 handler.setLevel(level)
46 handler.setFormatter(fmt)
47
48 logger.addHandler(handler)
49 logger.setLevel(level)
50
51 return logger
52
53 def valid_ip(self, ip):
54 if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip):
55 return True
56 else:
57 return False
58
59 def valid_url(self, url):
60 p = re.compile(
61 r'^(?:http|ftp)s?://'
62 r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # noqa
63 r'localhost|'
64 r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
65 r'(?::\d+)?'
66 r'(?:/?|[/?]\S+)$',
67 re.IGNORECASE)
68 if p.match(url):
69 return True
70 else:
71 return False
72
73 def validate_services(self, commands):
74 """Validate services.
75
76 Verify the specified services are running on the corresponding
77 service units.
78 """
79 for k, v in six.iteritems(commands):
80 for cmd in v:
81 output, code = k.run(cmd)
82 self.log.debug('{} `{}` returned '
83 '{}'.format(k.info['unit_name'],
84 cmd, code))
85 if code != 0:
86 return "command `{}` returned {}".format(cmd, str(code))
87 return None
88
89 def _get_config(self, unit, filename):
90 """Get a ConfigParser object for parsing a unit's config file."""
91 file_contents = unit.file_contents(filename)
92
93 # NOTE(beisner): by default, ConfigParser does not handle options
94 # with no value, such as the flags used in the mysql my.cnf file.
95 # https://bugs.python.org/issue7005
96 config = ConfigParser.ConfigParser(allow_no_value=True)
97 config.readfp(io.StringIO(file_contents))
98 return config
99
100 def validate_config_data(self, sentry_unit, config_file, section,
101 expected):
102 """Validate config file data.
103
104 Verify that the specified section of the config file contains
105 the expected option key:value pairs.
106 """
107 config = self._get_config(sentry_unit, config_file)
108
109 if section != 'DEFAULT' and not config.has_section(section):
110 return "section [{}] does not exist".format(section)
111
112 for k in expected.keys():
113 if not config.has_option(section, k):
114 return "section [{}] is missing option {}".format(section, k)
115 if config.get(section, k) != expected[k]:
116 return "section [{}] {}:{} != expected {}:{}".format(
117 section, k, config.get(section, k), k, expected[k])
118 return None
119
120 def _validate_dict_data(self, expected, actual):
121 """Validate dictionary data.
122
123 Compare expected dictionary data vs actual dictionary data.
124 The values in the 'expected' dictionary can be strings, bools, ints,
125 longs, or can be a function that evaluate a variable and returns a
126 bool.
127 """
128 self.log.debug('actual: {}'.format(repr(actual)))
129 self.log.debug('expected: {}'.format(repr(expected)))
130
131 for k, v in six.iteritems(expected):
132 if k in actual:
133 if (isinstance(v, six.string_types) or
134 isinstance(v, bool) or
135 isinstance(v, six.integer_types)):
136 if v != actual[k]:
137 return "{}:{}".format(k, actual[k])
138 elif not v(actual[k]):
139 return "{}:{}".format(k, actual[k])
140 else:
141 return "key '{}' does not exist".format(k)
142 return None
143
144 def validate_relation_data(self, sentry_unit, relation, expected):
145 """Validate actual relation data based on expected relation data."""
146 actual = sentry_unit.relation(relation[0], relation[1])
147 return self._validate_dict_data(expected, actual)
148
149 def _validate_list_data(self, expected, actual):
150 """Compare expected list vs actual list data."""
151 for e in expected:
152 if e not in actual:
153 return "expected item {} not found in actual list".format(e)
154 return None
155
156 def not_null(self, string):
157 if string is not None:
158 return True
159 else:
160 return False
161
162 def _get_file_mtime(self, sentry_unit, filename):
163 """Get last modification time of file."""
164 return sentry_unit.file_stat(filename)['mtime']
165
166 def _get_dir_mtime(self, sentry_unit, directory):
167 """Get last modification time of directory."""
168 return sentry_unit.directory_stat(directory)['mtime']
169
170 def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False):
171 """Get process' start time.
172
173 Determine start time of the process based on the last modification
174 time of the /proc/pid directory. If pgrep_full is True, the process
175 name is matched against the full command line.
176 """
177 if pgrep_full:
178 cmd = 'pgrep -o -f {}'.format(service)
179 else:
180 cmd = 'pgrep -o {}'.format(service)
181 cmd = cmd + ' | grep -v pgrep || exit 0'
182 cmd_out = sentry_unit.run(cmd)
183 self.log.debug('CMDout: ' + str(cmd_out))
184 if cmd_out[0]:
185 self.log.debug('Pid for %s %s' % (service, str(cmd_out[0])))
186 proc_dir = '/proc/{}'.format(cmd_out[0].strip())
187 return self._get_dir_mtime(sentry_unit, proc_dir)
188
189 def service_restarted(self, sentry_unit, service, filename,
190 pgrep_full=False, sleep_time=20):
191 """Check if service was restarted.
192
193 Compare a service's start time vs a file's last modification time
194 (such as a config file for that service) to determine if the service
195 has been restarted.
196 """
197 time.sleep(sleep_time)
198 if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
199 self._get_file_mtime(sentry_unit, filename)):
200 return True
201 else:
202 return False
203
204 def service_restarted_since(self, sentry_unit, mtime, service,
205 pgrep_full=False, sleep_time=20,
206 retry_count=2):
207 """Check if service was been started after a given time.
208
209 Args:
210 sentry_unit (sentry): The sentry unit to check for the service on
211 mtime (float): The epoch time to check against
212 service (string): service name to look for in process table
213 pgrep_full (boolean): Use full command line search mode with pgrep
214 sleep_time (int): Seconds to sleep before looking for process
215 retry_count (int): If service is not found, how many times to retry
216
217 Returns:
218 bool: True if service found and its start time it newer than mtime,
219 False if service is older than mtime or if service was
220 not found.
221 """
222 self.log.debug('Checking %s restarted since %s' % (service, mtime))
223 time.sleep(sleep_time)
224 proc_start_time = self._get_proc_start_time(sentry_unit, service,
225 pgrep_full)
226 while retry_count > 0 and not proc_start_time:
227 self.log.debug('No pid file found for service %s, will retry %i '
228 'more times' % (service, retry_count))
229 time.sleep(30)
230 proc_start_time = self._get_proc_start_time(sentry_unit, service,
231 pgrep_full)
232 retry_count = retry_count - 1
233
234 if not proc_start_time:
235 self.log.warn('No proc start time found, assuming service did '
236 'not start')
237 return False
238 if proc_start_time >= mtime:
239 self.log.debug('proc start time is newer than provided mtime'
240 '(%s >= %s)' % (proc_start_time, mtime))
241 return True
242 else:
243 self.log.warn('proc start time (%s) is older than provided mtime '
244 '(%s), service did not restart' % (proc_start_time,
245 mtime))
246 return False
247
248 def config_updated_since(self, sentry_unit, filename, mtime,
249 sleep_time=20):
250 """Check if file was modified after a given time.
251
252 Args:
253 sentry_unit (sentry): The sentry unit to check the file mtime on
254 filename (string): The file to check mtime of
255 mtime (float): The epoch time to check against
256 sleep_time (int): Seconds to sleep before looking for process
257
258 Returns:
259 bool: True if file was modified more recently than mtime, False if
260 file was modified before mtime,
261 """
262 self.log.debug('Checking %s updated since %s' % (filename, mtime))
263 time.sleep(sleep_time)
264 file_mtime = self._get_file_mtime(sentry_unit, filename)
265 if file_mtime >= mtime:
266 self.log.debug('File mtime is newer than provided mtime '
267 '(%s >= %s)' % (file_mtime, mtime))
268 return True
269 else:
270 self.log.warn('File mtime %s is older than provided mtime %s'
271 % (file_mtime, mtime))
272 return False
273
274 def validate_service_config_changed(self, sentry_unit, mtime, service,
275 filename, pgrep_full=False,
276 sleep_time=20, retry_count=2):
277 """Check service and file were updated after mtime
278
279 Args:
280 sentry_unit (sentry): The sentry unit to check for the service on
281 mtime (float): The epoch time to check against
282 service (string): service name to look for in process table
283 filename (string): The file to check mtime of
284 pgrep_full (boolean): Use full command line search mode with pgrep
285 sleep_time (int): Seconds to sleep before looking for process
286 retry_count (int): If service is not found, how many times to retry
287
288 Typical Usage:
289 u = OpenStackAmuletUtils(ERROR)
290 ...
291 mtime = u.get_sentry_time(self.cinder_sentry)
292 self.d.configure('cinder', {'verbose': 'True', 'debug': 'True'})
293 if not u.validate_service_config_changed(self.cinder_sentry,
294 mtime,
295 'cinder-api',
296 '/etc/cinder/cinder.conf')
297 amulet.raise_status(amulet.FAIL, msg='update failed')
298 Returns:
299 bool: True if both service and file where updated/restarted after
300 mtime, False if service is older than mtime or if service was
301 not found or if filename was modified before mtime.
302 """
303 self.log.debug('Checking %s restarted since %s' % (service, mtime))
304 time.sleep(sleep_time)
305 service_restart = self.service_restarted_since(sentry_unit, mtime,
306 service,
307 pgrep_full=pgrep_full,
308 sleep_time=0,
309 retry_count=retry_count)
310 config_update = self.config_updated_since(sentry_unit, filename, mtime,
311 sleep_time=0)
312 return service_restart and config_update
313
314 def get_sentry_time(self, sentry_unit):
315 """Return current epoch time on a sentry"""
316 cmd = "date +'%s'"
317 return float(sentry_unit.run(cmd)[0])
318
319 def relation_error(self, name, data):
320 return 'unexpected relation data in {} - {}'.format(name, data)
321
322 def endpoint_error(self, name, data):
323 return 'unexpected endpoint data in {} - {}'.format(name, data)
0324
=== added directory 'tests/charmhelpers/contrib/openstack'
=== added file 'tests/charmhelpers/contrib/openstack/__init__.py'
--- tests/charmhelpers/contrib/openstack/__init__.py 1970-01-01 00:00:00 +0000
+++ tests/charmhelpers/contrib/openstack/__init__.py 2015-10-19 03:23:14 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# 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/>.
016
=== added directory 'tests/charmhelpers/contrib/openstack/amulet'
=== added file 'tests/charmhelpers/contrib/openstack/amulet/__init__.py'
--- tests/charmhelpers/contrib/openstack/amulet/__init__.py 1970-01-01 00:00:00 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/__init__.py 2015-10-19 03:23:14 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# 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/>.
016
=== added file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 1970-01-01 00:00:00 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-10-19 03:23:14 +0000
@@ -0,0 +1,146 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# 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/>.
16
17import six
18from collections import OrderedDict
19from charmhelpers.contrib.amulet.deployment import (
20 AmuletDeployment
21)
22
23
24class OpenStackAmuletDeployment(AmuletDeployment):
25 """OpenStack amulet deployment.
26
27 This class inherits from AmuletDeployment and has additional support
28 that is specifically for use by OpenStack charms.
29 """
30
31 def __init__(self, series=None, openstack=None, source=None, stable=True):
32 """Initialize the deployment environment."""
33 super(OpenStackAmuletDeployment, self).__init__(series)
34 self.openstack = openstack
35 self.source = source
36 self.stable = stable
37 # Note(coreycb): this needs to be changed when new next branches come
38 # out.
39 self.current_next = "trusty"
40
41 def _determine_branch_locations(self, other_services):
42 """Determine the branch locations for the other services.
43
44 Determine if the local branch being tested is derived from its
45 stable or next (dev) branch, and based on this, use the corresonding
46 stable or next branches for the other_services."""
47 base_charms = ['mysql', 'mongodb']
48
49 if self.series in ['precise', 'trusty']:
50 base_series = self.series
51 else:
52 base_series = self.current_next
53
54 if self.stable:
55 for svc in other_services:
56 temp = 'lp:charms/{}/{}'
57 svc['location'] = temp.format(base_series,
58 svc['name'])
59 else:
60 for svc in other_services:
61 if svc['name'] in base_charms:
62 temp = 'lp:charms/{}/{}'
63 svc['location'] = temp.format(base_series,
64 svc['name'])
65 else:
66 temp = 'lp:~openstack-charmers/charms/{}/{}/next'
67 svc['location'] = temp.format(self.current_next,
68 svc['name'])
69 return other_services
70
71 def _add_services(self, this_service, other_services):
72 """Add services to the deployment and set openstack-origin/source."""
73 other_services = self._determine_branch_locations(other_services)
74
75 super(OpenStackAmuletDeployment, self)._add_services(this_service,
76 other_services)
77
78 services = other_services
79 services.append(this_service)
80 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
81 'ceph-osd', 'ceph-radosgw']
82 # Openstack subordinate charms do not expose an origin option as that
83 # is controlled by the principle
84 ignore = ['neutron-openvswitch']
85
86 if self.openstack:
87 for svc in services:
88 if svc['name'] not in use_source + ignore:
89 config = {'openstack-origin': self.openstack}
90 self.d.configure(svc['name'], config)
91
92 if self.source:
93 for svc in services:
94 if svc['name'] in use_source and svc['name'] not in ignore:
95 config = {'source': self.source}
96 self.d.configure(svc['name'], config)
97
98 def _configure_services(self, configs):
99 """Configure all of the services."""
100 for service, config in six.iteritems(configs):
101 self.d.configure(service, config)
102
103 def _get_openstack_release(self):
104 """Get openstack release.
105
106 Return an integer representing the enum value of the openstack
107 release.
108 """
109 # Must be ordered by OpenStack release (not by Ubuntu release):
110 (self.precise_essex, self.precise_folsom, self.precise_grizzly,
111 self.precise_havana, self.precise_icehouse,
112 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
113 self.trusty_kilo, self.vivid_kilo) = range(10)
114
115 releases = {
116 ('precise', None): self.precise_essex,
117 ('precise', 'cloud:precise-folsom'): self.precise_folsom,
118 ('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
119 ('precise', 'cloud:precise-havana'): self.precise_havana,
120 ('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
121 ('trusty', None): self.trusty_icehouse,
122 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
123 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
124 ('utopic', None): self.utopic_juno,
125 ('vivid', None): self.vivid_kilo}
126 return releases[(self.series, self.openstack)]
127
128 def _get_openstack_release_string(self):
129 """Get openstack release string.
130
131 Return a string representing the openstack release.
132 """
133 releases = OrderedDict([
134 ('precise', 'essex'),
135 ('quantal', 'folsom'),
136 ('raring', 'grizzly'),
137 ('saucy', 'havana'),
138 ('trusty', 'icehouse'),
139 ('utopic', 'juno'),
140 ('vivid', 'kilo'),
141 ])
142 if self.openstack:
143 os_origin = self.openstack.split(':')[1]
144 return os_origin.split('%s-' % self.series)[1].split('/')[0]
145 else:
146 return releases[self.series]
0147
=== added file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
--- tests/charmhelpers/contrib/openstack/amulet/utils.py 1970-01-01 00:00:00 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-10-19 03:23:14 +0000
@@ -0,0 +1,294 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# 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/>.
16
17import logging
18import os
19import time
20import urllib
21
22import glanceclient.v1.client as glance_client
23import keystoneclient.v2_0 as keystone_client
24import novaclient.v1_1.client as nova_client
25
26import six
27
28from charmhelpers.contrib.amulet.utils import (
29 AmuletUtils
30)
31
32DEBUG = logging.DEBUG
33ERROR = logging.ERROR
34
35
36class OpenStackAmuletUtils(AmuletUtils):
37 """OpenStack amulet utilities.
38
39 This class inherits from AmuletUtils and has additional support
40 that is specifically for use by OpenStack charms.
41 """
42
43 def __init__(self, log_level=ERROR):
44 """Initialize the deployment environment."""
45 super(OpenStackAmuletUtils, self).__init__(log_level)
46
47 def validate_endpoint_data(self, endpoints, admin_port, internal_port,
48 public_port, expected):
49 """Validate endpoint data.
50
51 Validate actual endpoint data vs expected endpoint data. The ports
52 are used to find the matching endpoint.
53 """
54 found = False
55 for ep in endpoints:
56 self.log.debug('endpoint: {}'.format(repr(ep)))
57 if (admin_port in ep.adminurl and
58 internal_port in ep.internalurl and
59 public_port in ep.publicurl):
60 found = True
61 actual = {'id': ep.id,
62 'region': ep.region,
63 'adminurl': ep.adminurl,
64 'internalurl': ep.internalurl,
65 'publicurl': ep.publicurl,
66 'service_id': ep.service_id}
67 ret = self._validate_dict_data(expected, actual)
68 if ret:
69 return 'unexpected endpoint data - {}'.format(ret)
70
71 if not found:
72 return 'endpoint not found'
73
74 def validate_svc_catalog_endpoint_data(self, expected, actual):
75 """Validate service catalog endpoint data.
76
77 Validate a list of actual service catalog endpoints vs a list of
78 expected service catalog endpoints.
79 """
80 self.log.debug('actual: {}'.format(repr(actual)))
81 for k, v in six.iteritems(expected):
82 if k in actual:
83 ret = self._validate_dict_data(expected[k][0], actual[k][0])
84 if ret:
85 return self.endpoint_error(k, ret)
86 else:
87 return "endpoint {} does not exist".format(k)
88 return ret
89
90 def validate_tenant_data(self, expected, actual):
91 """Validate tenant data.
92
93 Validate a list of actual tenant data vs list of expected tenant
94 data.
95 """
96 self.log.debug('actual: {}'.format(repr(actual)))
97 for e in expected:
98 found = False
99 for act in actual:
100 a = {'enabled': act.enabled, 'description': act.description,
101 'name': act.name, 'id': act.id}
102 if e['name'] == a['name']:
103 found = True
104 ret = self._validate_dict_data(e, a)
105 if ret:
106 return "unexpected tenant data - {}".format(ret)
107 if not found:
108 return "tenant {} does not exist".format(e['name'])
109 return ret
110
111 def validate_role_data(self, expected, actual):
112 """Validate role data.
113
114 Validate a list of actual role data vs a list of expected role
115 data.
116 """
117 self.log.debug('actual: {}'.format(repr(actual)))
118 for e in expected:
119 found = False
120 for act in actual:
121 a = {'name': act.name, 'id': act.id}
122 if e['name'] == a['name']:
123 found = True
124 ret = self._validate_dict_data(e, a)
125 if ret:
126 return "unexpected role data - {}".format(ret)
127 if not found:
128 return "role {} does not exist".format(e['name'])
129 return ret
130
131 def validate_user_data(self, expected, actual):
132 """Validate user data.
133
134 Validate a list of actual user data vs a list of expected user
135 data.
136 """
137 self.log.debug('actual: {}'.format(repr(actual)))
138 for e in expected:
139 found = False
140 for act in actual:
141 a = {'enabled': act.enabled, 'name': act.name,
142 'email': act.email, 'tenantId': act.tenantId,
143 'id': act.id}
144 if e['name'] == a['name']:
145 found = True
146 ret = self._validate_dict_data(e, a)
147 if ret:
148 return "unexpected user data - {}".format(ret)
149 if not found:
150 return "user {} does not exist".format(e['name'])
151 return ret
152
153 def validate_flavor_data(self, expected, actual):
154 """Validate flavor data.
155
156 Validate a list of actual flavors vs a list of expected flavors.
157 """
158 self.log.debug('actual: {}'.format(repr(actual)))
159 act = [a.name for a in actual]
160 return self._validate_list_data(expected, act)
161
162 def tenant_exists(self, keystone, tenant):
163 """Return True if tenant exists."""
164 return tenant in [t.name for t in keystone.tenants.list()]
165
166 def authenticate_keystone_admin(self, keystone_sentry, user, password,
167 tenant):
168 """Authenticates admin user with the keystone admin endpoint."""
169 unit = keystone_sentry
170 service_ip = unit.relation('shared-db',
171 'mysql:shared-db')['private-address']
172 ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
173 return keystone_client.Client(username=user, password=password,
174 tenant_name=tenant, auth_url=ep)
175
176 def authenticate_keystone_user(self, keystone, user, password, tenant):
177 """Authenticates a regular user with the keystone public endpoint."""
178 ep = keystone.service_catalog.url_for(service_type='identity',
179 endpoint_type='publicURL')
180 return keystone_client.Client(username=user, password=password,
181 tenant_name=tenant, auth_url=ep)
182
183 def authenticate_glance_admin(self, keystone):
184 """Authenticates admin user with glance."""
185 ep = keystone.service_catalog.url_for(service_type='image',
186 endpoint_type='adminURL')
187 return glance_client.Client(ep, token=keystone.auth_token)
188
189 def authenticate_nova_user(self, keystone, user, password, tenant):
190 """Authenticates a regular user with nova-api."""
191 ep = keystone.service_catalog.url_for(service_type='identity',
192 endpoint_type='publicURL')
193 return nova_client.Client(username=user, api_key=password,
194 project_id=tenant, auth_url=ep)
195
196 def create_cirros_image(self, glance, image_name):
197 """Download the latest cirros image and upload it to glance."""
198 http_proxy = os.getenv('AMULET_HTTP_PROXY')
199 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
200 if http_proxy:
201 proxies = {'http': http_proxy}
202 opener = urllib.FancyURLopener(proxies)
203 else:
204 opener = urllib.FancyURLopener()
205
206 f = opener.open("http://download.cirros-cloud.net/version/released")
207 version = f.read().strip()
208 cirros_img = "cirros-{}-x86_64-disk.img".format(version)
209 local_path = os.path.join('tests', cirros_img)
210
211 if not os.path.exists(local_path):
212 cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
213 version, cirros_img)
214 opener.retrieve(cirros_url, local_path)
215 f.close()
216
217 with open(local_path) as f:
218 image = glance.images.create(name=image_name, is_public=True,
219 disk_format='qcow2',
220 container_format='bare', data=f)
221 count = 1
222 status = image.status
223 while status != 'active' and count < 10:
224 time.sleep(3)
225 image = glance.images.get(image.id)
226 status = image.status
227 self.log.debug('image status: {}'.format(status))
228 count += 1
229
230 if status != 'active':
231 self.log.error('image creation timed out')
232 return None
233
234 return image
235
236 def delete_image(self, glance, image):
237 """Delete the specified image."""
238 num_before = len(list(glance.images.list()))
239 glance.images.delete(image)
240
241 count = 1
242 num_after = len(list(glance.images.list()))
243 while num_after != (num_before - 1) and count < 10:
244 time.sleep(3)
245 num_after = len(list(glance.images.list()))
246 self.log.debug('number of images: {}'.format(num_after))
247 count += 1
248
249 if num_after != (num_before - 1):
250 self.log.error('image deletion timed out')
251 return False
252
253 return True
254
255 def create_instance(self, nova, image_name, instance_name, flavor):
256 """Create the specified instance."""
257 image = nova.images.find(name=image_name)
258 flavor = nova.flavors.find(name=flavor)
259 instance = nova.servers.create(name=instance_name, image=image,
260 flavor=flavor)
261
262 count = 1
263 status = instance.status
264 while status != 'ACTIVE' and count < 60:
265 time.sleep(3)
266 instance = nova.servers.get(instance.id)
267 status = instance.status
268 self.log.debug('instance status: {}'.format(status))
269 count += 1
270
271 if status != 'ACTIVE':
272 self.log.error('instance creation timed out')
273 return None
274
275 return instance
276
277 def delete_instance(self, nova, instance):
278 """Delete the specified instance."""
279 num_before = len(list(nova.servers.list()))
280 nova.servers.delete(instance)
281
282 count = 1
283 num_after = len(list(nova.servers.list()))
284 while num_after != (num_before - 1) and count < 10:
285 time.sleep(3)
286 num_after = len(list(nova.servers.list()))
287 self.log.debug('number of instances: {}'.format(num_after))
288 count += 1
289
290 if num_after != (num_before - 1):
291 self.log.error('instance deletion timed out')
292 return False
293
294 return True

Subscribers

People subscribed via source and target branches