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