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