Merge lp:~corey.bryant/charms/trusty/neutron-api/fix-global-reqs into lp:~openstack-charmers-archive/charms/trusty/neutron-api/next
- Trusty Tahr (14.04)
- fix-global-reqs
- Merge into next
Proposed by
Corey Bryant
Status: | Merged |
---|---|
Approved by: | Billy Olsen |
Approved revision: | 119 |
Merged at revision: | 119 |
Proposed branch: | lp:~corey.bryant/charms/trusty/neutron-api/fix-global-reqs |
Merge into: | lp:~openstack-charmers-archive/charms/trusty/neutron-api/next |
Diff against target: |
969 lines (+411/-38) 10 files modified
hooks/charmhelpers/contrib/hahelpers/cluster.py (+12/-3) hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+6/-2) hooks/charmhelpers/contrib/openstack/amulet/utils.py (+122/-3) hooks/charmhelpers/contrib/openstack/context.py (+1/-1) hooks/charmhelpers/contrib/openstack/neutron.py (+6/-4) hooks/charmhelpers/contrib/openstack/utils.py (+21/-8) hooks/charmhelpers/core/host.py (+24/-6) tests/charmhelpers/contrib/amulet/utils.py (+91/-6) tests/charmhelpers/contrib/openstack/amulet/deployment.py (+6/-2) tests/charmhelpers/contrib/openstack/amulet/utils.py (+122/-3) |
To merge this branch: | bzr merge lp:~corey.bryant/charms/trusty/neutron-api/fix-global-reqs |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Billy Olsen | Approve | ||
Review via email: mp+262471@code.launchpad.net |
Commit message
Description of the change
See https:/
for details explaining this merge.
To post a comment you must log in.
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : | # |
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_unit_test #5106 neutron-api-next for corey.bryant mp262471
UNIT OK: passed
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_amulet_test #4681 neutron-api-next for corey.bryant mp262471
AMULET OK: passed
Build: http://
Revision history for this message
Billy Olsen (billy-olsen) wrote : | # |
LGTM, Approved
review:
Approve
Revision history for this message
Billy Olsen (billy-olsen) wrote : | # |
LGTM, Approved
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'hooks/charmhelpers/contrib/hahelpers/cluster.py' |
2 | --- hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-06-04 08:44:57 +0000 |
3 | +++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-06-19 16:15:27 +0000 |
4 | @@ -64,6 +64,10 @@ |
5 | pass |
6 | |
7 | |
8 | +class CRMDCNotFound(Exception): |
9 | + pass |
10 | + |
11 | + |
12 | def is_elected_leader(resource): |
13 | """ |
14 | Returns True if the charm executing this is the elected cluster leader. |
15 | @@ -116,8 +120,9 @@ |
16 | status = subprocess.check_output(cmd, stderr=subprocess.STDOUT) |
17 | if not isinstance(status, six.text_type): |
18 | status = six.text_type(status, "utf-8") |
19 | - except subprocess.CalledProcessError: |
20 | - return False |
21 | + except subprocess.CalledProcessError as ex: |
22 | + raise CRMDCNotFound(str(ex)) |
23 | + |
24 | current_dc = '' |
25 | for line in status.split('\n'): |
26 | if line.startswith('Current DC'): |
27 | @@ -125,10 +130,14 @@ |
28 | current_dc = line.split(':')[1].split()[0] |
29 | if current_dc == get_unit_hostname(): |
30 | return True |
31 | + elif current_dc == 'NONE': |
32 | + raise CRMDCNotFound('Current DC: NONE') |
33 | + |
34 | return False |
35 | |
36 | |
37 | -@retry_on_exception(5, base_delay=2, exc_type=CRMResourceNotFound) |
38 | +@retry_on_exception(5, base_delay=2, |
39 | + exc_type=(CRMResourceNotFound, CRMDCNotFound)) |
40 | def is_crm_leader(resource, retry=False): |
41 | """ |
42 | Returns True if the charm calling this is the elected corosync leader, |
43 | |
44 | === modified file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py' |
45 | --- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-10 14:01:05 +0000 |
46 | +++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-19 16:15:27 +0000 |
47 | @@ -110,7 +110,8 @@ |
48 | (self.precise_essex, self.precise_folsom, self.precise_grizzly, |
49 | self.precise_havana, self.precise_icehouse, |
50 | self.trusty_icehouse, self.trusty_juno, self.utopic_juno, |
51 | - self.trusty_kilo, self.vivid_kilo) = range(10) |
52 | + self.trusty_kilo, self.vivid_kilo, self.trusty_liberty, |
53 | + self.wily_liberty) = range(12) |
54 | |
55 | releases = { |
56 | ('precise', None): self.precise_essex, |
57 | @@ -121,8 +122,10 @@ |
58 | ('trusty', None): self.trusty_icehouse, |
59 | ('trusty', 'cloud:trusty-juno'): self.trusty_juno, |
60 | ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo, |
61 | + ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty, |
62 | ('utopic', None): self.utopic_juno, |
63 | - ('vivid', None): self.vivid_kilo} |
64 | + ('vivid', None): self.vivid_kilo, |
65 | + ('wily', None): self.wily_liberty} |
66 | return releases[(self.series, self.openstack)] |
67 | |
68 | def _get_openstack_release_string(self): |
69 | @@ -138,6 +141,7 @@ |
70 | ('trusty', 'icehouse'), |
71 | ('utopic', 'juno'), |
72 | ('vivid', 'kilo'), |
73 | + ('wily', 'liberty'), |
74 | ]) |
75 | if self.openstack: |
76 | os_origin = self.openstack.split(':')[1] |
77 | |
78 | === modified file 'hooks/charmhelpers/contrib/openstack/amulet/utils.py' |
79 | --- hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-02-24 11:40:25 +0000 |
80 | +++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-06-19 16:15:27 +0000 |
81 | @@ -16,15 +16,15 @@ |
82 | |
83 | import logging |
84 | import os |
85 | +import six |
86 | import time |
87 | import urllib |
88 | |
89 | import glanceclient.v1.client as glance_client |
90 | +import heatclient.v1.client as heat_client |
91 | import keystoneclient.v2_0 as keystone_client |
92 | import novaclient.v1_1.client as nova_client |
93 | |
94 | -import six |
95 | - |
96 | from charmhelpers.contrib.amulet.utils import ( |
97 | AmuletUtils |
98 | ) |
99 | @@ -37,7 +37,7 @@ |
100 | """OpenStack amulet utilities. |
101 | |
102 | This class inherits from AmuletUtils and has additional support |
103 | - that is specifically for use by OpenStack charms. |
104 | + that is specifically for use by OpenStack charm tests. |
105 | """ |
106 | |
107 | def __init__(self, log_level=ERROR): |
108 | @@ -51,6 +51,8 @@ |
109 | Validate actual endpoint data vs expected endpoint data. The ports |
110 | are used to find the matching endpoint. |
111 | """ |
112 | + self.log.debug('Validating endpoint data...') |
113 | + self.log.debug('actual: {}'.format(repr(endpoints))) |
114 | found = False |
115 | for ep in endpoints: |
116 | self.log.debug('endpoint: {}'.format(repr(ep))) |
117 | @@ -77,6 +79,7 @@ |
118 | Validate a list of actual service catalog endpoints vs a list of |
119 | expected service catalog endpoints. |
120 | """ |
121 | + self.log.debug('Validating service catalog endpoint data...') |
122 | self.log.debug('actual: {}'.format(repr(actual))) |
123 | for k, v in six.iteritems(expected): |
124 | if k in actual: |
125 | @@ -93,6 +96,7 @@ |
126 | Validate a list of actual tenant data vs list of expected tenant |
127 | data. |
128 | """ |
129 | + self.log.debug('Validating tenant data...') |
130 | self.log.debug('actual: {}'.format(repr(actual))) |
131 | for e in expected: |
132 | found = False |
133 | @@ -114,6 +118,7 @@ |
134 | Validate a list of actual role data vs a list of expected role |
135 | data. |
136 | """ |
137 | + self.log.debug('Validating role data...') |
138 | self.log.debug('actual: {}'.format(repr(actual))) |
139 | for e in expected: |
140 | found = False |
141 | @@ -134,6 +139,7 @@ |
142 | Validate a list of actual user data vs a list of expected user |
143 | data. |
144 | """ |
145 | + self.log.debug('Validating user data...') |
146 | self.log.debug('actual: {}'.format(repr(actual))) |
147 | for e in expected: |
148 | found = False |
149 | @@ -155,17 +161,20 @@ |
150 | |
151 | Validate a list of actual flavors vs a list of expected flavors. |
152 | """ |
153 | + self.log.debug('Validating flavor data...') |
154 | self.log.debug('actual: {}'.format(repr(actual))) |
155 | act = [a.name for a in actual] |
156 | return self._validate_list_data(expected, act) |
157 | |
158 | def tenant_exists(self, keystone, tenant): |
159 | """Return True if tenant exists.""" |
160 | + self.log.debug('Checking if tenant exists ({})...'.format(tenant)) |
161 | return tenant in [t.name for t in keystone.tenants.list()] |
162 | |
163 | def authenticate_keystone_admin(self, keystone_sentry, user, password, |
164 | tenant): |
165 | """Authenticates admin user with the keystone admin endpoint.""" |
166 | + self.log.debug('Authenticating keystone admin...') |
167 | unit = keystone_sentry |
168 | service_ip = unit.relation('shared-db', |
169 | 'mysql:shared-db')['private-address'] |
170 | @@ -175,6 +184,7 @@ |
171 | |
172 | def authenticate_keystone_user(self, keystone, user, password, tenant): |
173 | """Authenticates a regular user with the keystone public endpoint.""" |
174 | + self.log.debug('Authenticating keystone user ({})...'.format(user)) |
175 | ep = keystone.service_catalog.url_for(service_type='identity', |
176 | endpoint_type='publicURL') |
177 | return keystone_client.Client(username=user, password=password, |
178 | @@ -182,12 +192,21 @@ |
179 | |
180 | def authenticate_glance_admin(self, keystone): |
181 | """Authenticates admin user with glance.""" |
182 | + self.log.debug('Authenticating glance admin...') |
183 | ep = keystone.service_catalog.url_for(service_type='image', |
184 | endpoint_type='adminURL') |
185 | return glance_client.Client(ep, token=keystone.auth_token) |
186 | |
187 | + def authenticate_heat_admin(self, keystone): |
188 | + """Authenticates the admin user with heat.""" |
189 | + self.log.debug('Authenticating heat admin...') |
190 | + ep = keystone.service_catalog.url_for(service_type='orchestration', |
191 | + endpoint_type='publicURL') |
192 | + return heat_client.Client(endpoint=ep, token=keystone.auth_token) |
193 | + |
194 | def authenticate_nova_user(self, keystone, user, password, tenant): |
195 | """Authenticates a regular user with nova-api.""" |
196 | + self.log.debug('Authenticating nova user ({})...'.format(user)) |
197 | ep = keystone.service_catalog.url_for(service_type='identity', |
198 | endpoint_type='publicURL') |
199 | return nova_client.Client(username=user, api_key=password, |
200 | @@ -195,6 +214,7 @@ |
201 | |
202 | def create_cirros_image(self, glance, image_name): |
203 | """Download the latest cirros image and upload it to glance.""" |
204 | + self.log.debug('Creating glance image ({})...'.format(image_name)) |
205 | http_proxy = os.getenv('AMULET_HTTP_PROXY') |
206 | self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy)) |
207 | if http_proxy: |
208 | @@ -235,6 +255,11 @@ |
209 | |
210 | def delete_image(self, glance, image): |
211 | """Delete the specified image.""" |
212 | + |
213 | + # /!\ DEPRECATION WARNING |
214 | + self.log.warn('/!\\ DEPRECATION WARNING: use ' |
215 | + 'delete_resource instead of delete_image.') |
216 | + self.log.debug('Deleting glance image ({})...'.format(image)) |
217 | num_before = len(list(glance.images.list())) |
218 | glance.images.delete(image) |
219 | |
220 | @@ -254,6 +279,8 @@ |
221 | |
222 | def create_instance(self, nova, image_name, instance_name, flavor): |
223 | """Create the specified instance.""" |
224 | + self.log.debug('Creating instance ' |
225 | + '({}|{}|{})'.format(instance_name, image_name, flavor)) |
226 | image = nova.images.find(name=image_name) |
227 | flavor = nova.flavors.find(name=flavor) |
228 | instance = nova.servers.create(name=instance_name, image=image, |
229 | @@ -276,6 +303,11 @@ |
230 | |
231 | def delete_instance(self, nova, instance): |
232 | """Delete the specified instance.""" |
233 | + |
234 | + # /!\ DEPRECATION WARNING |
235 | + self.log.warn('/!\\ DEPRECATION WARNING: use ' |
236 | + 'delete_resource instead of delete_instance.') |
237 | + self.log.debug('Deleting instance ({})...'.format(instance)) |
238 | num_before = len(list(nova.servers.list())) |
239 | nova.servers.delete(instance) |
240 | |
241 | @@ -292,3 +324,90 @@ |
242 | return False |
243 | |
244 | return True |
245 | + |
246 | + def create_or_get_keypair(self, nova, keypair_name="testkey"): |
247 | + """Create a new keypair, or return pointer if it already exists.""" |
248 | + try: |
249 | + _keypair = nova.keypairs.get(keypair_name) |
250 | + self.log.debug('Keypair ({}) already exists, ' |
251 | + 'using it.'.format(keypair_name)) |
252 | + return _keypair |
253 | + except: |
254 | + self.log.debug('Keypair ({}) does not exist, ' |
255 | + 'creating it.'.format(keypair_name)) |
256 | + |
257 | + _keypair = nova.keypairs.create(name=keypair_name) |
258 | + return _keypair |
259 | + |
260 | + def delete_resource(self, resource, resource_id, |
261 | + msg="resource", max_wait=120): |
262 | + """Delete one openstack resource, such as one instance, keypair, |
263 | + image, volume, stack, etc., and confirm deletion within max wait time. |
264 | + |
265 | + :param resource: pointer to os resource type, ex:glance_client.images |
266 | + :param resource_id: unique name or id for the openstack resource |
267 | + :param msg: text to identify purpose in logging |
268 | + :param max_wait: maximum wait time in seconds |
269 | + :returns: True if successful, otherwise False |
270 | + """ |
271 | + num_before = len(list(resource.list())) |
272 | + resource.delete(resource_id) |
273 | + |
274 | + tries = 0 |
275 | + num_after = len(list(resource.list())) |
276 | + while num_after != (num_before - 1) and tries < (max_wait / 4): |
277 | + self.log.debug('{} delete check: ' |
278 | + '{} [{}:{}] {}'.format(msg, tries, |
279 | + num_before, |
280 | + num_after, |
281 | + resource_id)) |
282 | + time.sleep(4) |
283 | + num_after = len(list(resource.list())) |
284 | + tries += 1 |
285 | + |
286 | + self.log.debug('{}: expected, actual count = {}, ' |
287 | + '{}'.format(msg, num_before - 1, num_after)) |
288 | + |
289 | + if num_after == (num_before - 1): |
290 | + return True |
291 | + else: |
292 | + self.log.error('{} delete timed out'.format(msg)) |
293 | + return False |
294 | + |
295 | + def resource_reaches_status(self, resource, resource_id, |
296 | + expected_stat='available', |
297 | + msg='resource', max_wait=120): |
298 | + """Wait for an openstack resources status to reach an |
299 | + expected status within a specified time. Useful to confirm that |
300 | + nova instances, cinder vols, snapshots, glance images, heat stacks |
301 | + and other resources eventually reach the expected status. |
302 | + |
303 | + :param resource: pointer to os resource type, ex: heat_client.stacks |
304 | + :param resource_id: unique id for the openstack resource |
305 | + :param expected_stat: status to expect resource to reach |
306 | + :param msg: text to identify purpose in logging |
307 | + :param max_wait: maximum wait time in seconds |
308 | + :returns: True if successful, False if status is not reached |
309 | + """ |
310 | + |
311 | + tries = 0 |
312 | + resource_stat = resource.get(resource_id).status |
313 | + while resource_stat != expected_stat and tries < (max_wait / 4): |
314 | + self.log.debug('{} status check: ' |
315 | + '{} [{}:{}] {}'.format(msg, tries, |
316 | + resource_stat, |
317 | + expected_stat, |
318 | + resource_id)) |
319 | + time.sleep(4) |
320 | + resource_stat = resource.get(resource_id).status |
321 | + tries += 1 |
322 | + |
323 | + self.log.debug('{}: expected, actual status = {}, ' |
324 | + '{}'.format(msg, resource_stat, expected_stat)) |
325 | + |
326 | + if resource_stat == expected_stat: |
327 | + return True |
328 | + else: |
329 | + self.log.debug('{} never reached expected status: ' |
330 | + '{}'.format(resource_id, expected_stat)) |
331 | + return False |
332 | |
333 | === modified file 'hooks/charmhelpers/contrib/openstack/context.py' |
334 | --- hooks/charmhelpers/contrib/openstack/context.py 2015-05-11 07:38:23 +0000 |
335 | +++ hooks/charmhelpers/contrib/openstack/context.py 2015-06-19 16:15:27 +0000 |
336 | @@ -240,7 +240,7 @@ |
337 | if self.relation_prefix: |
338 | password_setting = self.relation_prefix + '_password' |
339 | |
340 | - for rid in relation_ids('shared-db'): |
341 | + for rid in relation_ids(self.interfaces[0]): |
342 | for unit in related_units(rid): |
343 | rdata = relation_get(rid=rid, unit=unit) |
344 | host = rdata.get('db_host') |
345 | |
346 | === modified file 'hooks/charmhelpers/contrib/openstack/neutron.py' |
347 | --- hooks/charmhelpers/contrib/openstack/neutron.py 2015-06-04 08:44:57 +0000 |
348 | +++ hooks/charmhelpers/contrib/openstack/neutron.py 2015-06-19 16:15:27 +0000 |
349 | @@ -172,14 +172,16 @@ |
350 | 'services': ['calico-felix', |
351 | 'bird', |
352 | 'neutron-dhcp-agent', |
353 | - 'nova-api-metadata'], |
354 | + 'nova-api-metadata', |
355 | + 'etcd'], |
356 | 'packages': [[headers_package()] + determine_dkms_package(), |
357 | ['calico-compute', |
358 | 'bird', |
359 | 'neutron-dhcp-agent', |
360 | - 'nova-api-metadata']], |
361 | - 'server_packages': ['neutron-server', 'calico-control'], |
362 | - 'server_services': ['neutron-server'] |
363 | + 'nova-api-metadata', |
364 | + 'etcd']], |
365 | + 'server_packages': ['neutron-server', 'calico-control', 'etcd'], |
366 | + 'server_services': ['neutron-server', 'etcd'] |
367 | }, |
368 | 'vsp': { |
369 | 'config': '/etc/neutron/plugins/nuage/nuage_plugin.ini', |
370 | |
371 | === modified file 'hooks/charmhelpers/contrib/openstack/utils.py' |
372 | --- hooks/charmhelpers/contrib/openstack/utils.py 2015-06-10 14:01:05 +0000 |
373 | +++ hooks/charmhelpers/contrib/openstack/utils.py 2015-06-19 16:15:27 +0000 |
374 | @@ -79,6 +79,7 @@ |
375 | ('trusty', 'icehouse'), |
376 | ('utopic', 'juno'), |
377 | ('vivid', 'kilo'), |
378 | + ('wily', 'liberty'), |
379 | ]) |
380 | |
381 | |
382 | @@ -91,6 +92,7 @@ |
383 | ('2014.1', 'icehouse'), |
384 | ('2014.2', 'juno'), |
385 | ('2015.1', 'kilo'), |
386 | + ('2015.2', 'liberty'), |
387 | ]) |
388 | |
389 | # The ugly duckling |
390 | @@ -113,6 +115,7 @@ |
391 | ('2.2.0', 'juno'), |
392 | ('2.2.1', 'kilo'), |
393 | ('2.2.2', 'kilo'), |
394 | + ('2.3.0', 'liberty'), |
395 | ]) |
396 | |
397 | DEFAULT_LOOPBACK_SIZE = '5G' |
398 | @@ -321,6 +324,9 @@ |
399 | 'kilo': 'trusty-updates/kilo', |
400 | 'kilo/updates': 'trusty-updates/kilo', |
401 | 'kilo/proposed': 'trusty-proposed/kilo', |
402 | + 'liberty': 'trusty-updates/liberty', |
403 | + 'liberty/updates': 'trusty-updates/liberty', |
404 | + 'liberty/proposed': 'trusty-proposed/liberty', |
405 | } |
406 | |
407 | try: |
408 | @@ -549,6 +555,11 @@ |
409 | |
410 | pip_create_virtualenv(os.path.join(parent_dir, 'venv')) |
411 | |
412 | + # Upgrade setuptools from default virtualenv version. The default version |
413 | + # in trusty breaks update.py in global requirements master branch. |
414 | + pip_install('setuptools', upgrade=True, proxy=http_proxy, |
415 | + venv=os.path.join(parent_dir, 'venv')) |
416 | + |
417 | for p in projects['repositories']: |
418 | repo = p['repository'] |
419 | branch = p['branch'] |
420 | @@ -610,24 +621,24 @@ |
421 | else: |
422 | repo_dir = dest_dir |
423 | |
424 | + venv = os.path.join(parent_dir, 'venv') |
425 | + |
426 | if update_requirements: |
427 | if not requirements_dir: |
428 | error_out('requirements repo must be cloned before ' |
429 | 'updating from global requirements.') |
430 | - _git_update_requirements(repo_dir, requirements_dir) |
431 | + _git_update_requirements(venv, repo_dir, requirements_dir) |
432 | |
433 | juju_log('Installing git repo from dir: {}'.format(repo_dir)) |
434 | if http_proxy: |
435 | - pip_install(repo_dir, proxy=http_proxy, |
436 | - venv=os.path.join(parent_dir, 'venv')) |
437 | + pip_install(repo_dir, proxy=http_proxy, venv=venv) |
438 | else: |
439 | - pip_install(repo_dir, |
440 | - venv=os.path.join(parent_dir, 'venv')) |
441 | + pip_install(repo_dir, venv=venv) |
442 | |
443 | return repo_dir |
444 | |
445 | |
446 | -def _git_update_requirements(package_dir, reqs_dir): |
447 | +def _git_update_requirements(venv, package_dir, reqs_dir): |
448 | """ |
449 | Update from global requirements. |
450 | |
451 | @@ -636,12 +647,14 @@ |
452 | """ |
453 | orig_dir = os.getcwd() |
454 | os.chdir(reqs_dir) |
455 | - cmd = ['python', 'update.py', package_dir] |
456 | + python = os.path.join(venv, 'bin/python') |
457 | + cmd = [python, 'update.py', package_dir] |
458 | try: |
459 | subprocess.check_call(cmd) |
460 | except subprocess.CalledProcessError: |
461 | package = os.path.basename(package_dir) |
462 | - error_out("Error updating {} from global-requirements.txt".format(package)) |
463 | + error_out("Error updating {} from " |
464 | + "global-requirements.txt".format(package)) |
465 | os.chdir(orig_dir) |
466 | |
467 | |
468 | |
469 | === modified file 'hooks/charmhelpers/core/host.py' |
470 | --- hooks/charmhelpers/core/host.py 2015-06-04 08:44:57 +0000 |
471 | +++ hooks/charmhelpers/core/host.py 2015-06-19 16:15:27 +0000 |
472 | @@ -24,6 +24,7 @@ |
473 | import os |
474 | import re |
475 | import pwd |
476 | +import glob |
477 | import grp |
478 | import random |
479 | import string |
480 | @@ -269,6 +270,21 @@ |
481 | return None |
482 | |
483 | |
484 | +def path_hash(path): |
485 | + """ |
486 | + Generate a hash checksum of all files matching 'path'. Standard wildcards |
487 | + like '*' and '?' are supported, see documentation for the 'glob' module for |
488 | + more information. |
489 | + |
490 | + :return: dict: A { filename: hash } dictionary for all matched files. |
491 | + Empty if none found. |
492 | + """ |
493 | + return { |
494 | + filename: file_hash(filename) |
495 | + for filename in glob.iglob(path) |
496 | + } |
497 | + |
498 | + |
499 | def check_hash(path, checksum, hash_type='md5'): |
500 | """ |
501 | Validate a file using a cryptographic checksum. |
502 | @@ -296,23 +312,25 @@ |
503 | |
504 | @restart_on_change({ |
505 | '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ] |
506 | + '/etc/apache/sites-enabled/*': [ 'apache2' ] |
507 | }) |
508 | - def ceph_client_changed(): |
509 | + def config_changed(): |
510 | pass # your code here |
511 | |
512 | In this example, the cinder-api and cinder-volume services |
513 | would be restarted if /etc/ceph/ceph.conf is changed by the |
514 | - ceph_client_changed function. |
515 | + ceph_client_changed function. The apache2 service would be |
516 | + restarted if any file matching the pattern got changed, created |
517 | + or removed. Standard wildcards are supported, see documentation |
518 | + for the 'glob' module for more information. |
519 | """ |
520 | def wrap(f): |
521 | def wrapped_f(*args, **kwargs): |
522 | - checksums = {} |
523 | - for path in restart_map: |
524 | - checksums[path] = file_hash(path) |
525 | + checksums = {path: path_hash(path) for path in restart_map} |
526 | f(*args, **kwargs) |
527 | restarts = [] |
528 | for path in restart_map: |
529 | - if checksums[path] != file_hash(path): |
530 | + if path_hash(path) != checksums[path]: |
531 | restarts += restart_map[path] |
532 | services_list = list(OrderedDict.fromkeys(restarts)) |
533 | if not stopstart: |
534 | |
535 | === modified file 'tests/charmhelpers/contrib/amulet/utils.py' |
536 | --- tests/charmhelpers/contrib/amulet/utils.py 2015-05-05 20:25:52 +0000 |
537 | +++ tests/charmhelpers/contrib/amulet/utils.py 2015-06-19 16:15:27 +0000 |
538 | @@ -15,13 +15,15 @@ |
539 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
540 | |
541 | import ConfigParser |
542 | +import distro_info |
543 | import io |
544 | import logging |
545 | +import os |
546 | import re |
547 | +import six |
548 | import sys |
549 | import time |
550 | - |
551 | -import six |
552 | +import urlparse |
553 | |
554 | |
555 | class AmuletUtils(object): |
556 | @@ -33,6 +35,7 @@ |
557 | |
558 | def __init__(self, log_level=logging.ERROR): |
559 | self.log = self.get_logger(level=log_level) |
560 | + self.ubuntu_releases = self.get_ubuntu_releases() |
561 | |
562 | def get_logger(self, name="amulet-logger", level=logging.DEBUG): |
563 | """Get a logger object that will log to stdout.""" |
564 | @@ -70,12 +73,44 @@ |
565 | else: |
566 | return False |
567 | |
568 | + def get_ubuntu_release_from_sentry(self, sentry_unit): |
569 | + """Get Ubuntu release codename from sentry unit. |
570 | + |
571 | + :param sentry_unit: amulet sentry/service unit pointer |
572 | + :returns: list of strings - release codename, failure message |
573 | + """ |
574 | + msg = None |
575 | + cmd = 'lsb_release -cs' |
576 | + release, code = sentry_unit.run(cmd) |
577 | + if code == 0: |
578 | + self.log.debug('{} lsb_release: {}'.format( |
579 | + sentry_unit.info['unit_name'], release)) |
580 | + else: |
581 | + msg = ('{} `{}` returned {} ' |
582 | + '{}'.format(sentry_unit.info['unit_name'], |
583 | + cmd, release, code)) |
584 | + if release not in self.ubuntu_releases: |
585 | + msg = ("Release ({}) not found in Ubuntu releases " |
586 | + "({})".format(release, self.ubuntu_releases)) |
587 | + return release, msg |
588 | + |
589 | def validate_services(self, commands): |
590 | - """Validate services. |
591 | - |
592 | - Verify the specified services are running on the corresponding |
593 | + """Validate that lists of commands succeed on service units. Can be |
594 | + used to verify system services are running on the corresponding |
595 | service units. |
596 | - """ |
597 | + |
598 | + :param commands: dict with sentry keys and arbitrary command list vals |
599 | + :returns: None if successful, Failure string message otherwise |
600 | + """ |
601 | + self.log.debug('Checking status of system services...') |
602 | + |
603 | + # /!\ DEPRECATION WARNING (beisner): |
604 | + # New and existing tests should be rewritten to use |
605 | + # validate_services_by_name() as it is aware of init systems. |
606 | + self.log.warn('/!\\ DEPRECATION WARNING: use ' |
607 | + 'validate_services_by_name instead of validate_services ' |
608 | + 'due to init system differences.') |
609 | + |
610 | for k, v in six.iteritems(commands): |
611 | for cmd in v: |
612 | output, code = k.run(cmd) |
613 | @@ -86,6 +121,41 @@ |
614 | return "command `{}` returned {}".format(cmd, str(code)) |
615 | return None |
616 | |
617 | + def validate_services_by_name(self, sentry_services): |
618 | + """Validate system service status by service name, automatically |
619 | + detecting init system based on Ubuntu release codename. |
620 | + |
621 | + :param sentry_services: dict with sentry keys and svc list values |
622 | + :returns: None if successful, Failure string message otherwise |
623 | + """ |
624 | + self.log.debug('Checking status of system services...') |
625 | + |
626 | + # Point at which systemd became a thing |
627 | + systemd_switch = self.ubuntu_releases.index('vivid') |
628 | + |
629 | + for sentry_unit, services_list in six.iteritems(sentry_services): |
630 | + # Get lsb_release codename from unit |
631 | + release, ret = self.get_ubuntu_release_from_sentry(sentry_unit) |
632 | + if ret: |
633 | + return ret |
634 | + |
635 | + for service_name in services_list: |
636 | + if (self.ubuntu_releases.index(release) >= systemd_switch or |
637 | + service_name == "rabbitmq-server"): |
638 | + # init is systemd |
639 | + cmd = 'sudo service {} status'.format(service_name) |
640 | + elif self.ubuntu_releases.index(release) < systemd_switch: |
641 | + # init is upstart |
642 | + cmd = 'sudo status {}'.format(service_name) |
643 | + |
644 | + output, code = sentry_unit.run(cmd) |
645 | + self.log.debug('{} `{}` returned ' |
646 | + '{}'.format(sentry_unit.info['unit_name'], |
647 | + cmd, code)) |
648 | + if code != 0: |
649 | + return "command `{}` returned {}".format(cmd, str(code)) |
650 | + return None |
651 | + |
652 | def _get_config(self, unit, filename): |
653 | """Get a ConfigParser object for parsing a unit's config file.""" |
654 | file_contents = unit.file_contents(filename) |
655 | @@ -104,6 +174,9 @@ |
656 | Verify that the specified section of the config file contains |
657 | the expected option key:value pairs. |
658 | """ |
659 | + self.log.debug('Validating config file data ({} in {} on {})' |
660 | + '...'.format(section, config_file, |
661 | + sentry_unit.info['unit_name'])) |
662 | config = self._get_config(sentry_unit, config_file) |
663 | |
664 | if section != 'DEFAULT' and not config.has_section(section): |
665 | @@ -321,3 +394,15 @@ |
666 | |
667 | def endpoint_error(self, name, data): |
668 | return 'unexpected endpoint data in {} - {}'.format(name, data) |
669 | + |
670 | + def get_ubuntu_releases(self): |
671 | + """Return a list of all Ubuntu releases in order of release.""" |
672 | + _d = distro_info.UbuntuDistroInfo() |
673 | + _release_list = _d.all |
674 | + self.log.debug('Ubuntu release list: {}'.format(_release_list)) |
675 | + return _release_list |
676 | + |
677 | + def file_to_url(self, file_rel_path): |
678 | + """Convert a relative file path to a file URL.""" |
679 | + _abs_path = os.path.abspath(file_rel_path) |
680 | + return urlparse.urlparse(_abs_path, scheme='file').geturl() |
681 | |
682 | === modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py' |
683 | --- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-10 14:01:05 +0000 |
684 | +++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-19 16:15:27 +0000 |
685 | @@ -110,7 +110,8 @@ |
686 | (self.precise_essex, self.precise_folsom, self.precise_grizzly, |
687 | self.precise_havana, self.precise_icehouse, |
688 | self.trusty_icehouse, self.trusty_juno, self.utopic_juno, |
689 | - self.trusty_kilo, self.vivid_kilo) = range(10) |
690 | + self.trusty_kilo, self.vivid_kilo, self.trusty_liberty, |
691 | + self.wily_liberty) = range(12) |
692 | |
693 | releases = { |
694 | ('precise', None): self.precise_essex, |
695 | @@ -121,8 +122,10 @@ |
696 | ('trusty', None): self.trusty_icehouse, |
697 | ('trusty', 'cloud:trusty-juno'): self.trusty_juno, |
698 | ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo, |
699 | + ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty, |
700 | ('utopic', None): self.utopic_juno, |
701 | - ('vivid', None): self.vivid_kilo} |
702 | + ('vivid', None): self.vivid_kilo, |
703 | + ('wily', None): self.wily_liberty} |
704 | return releases[(self.series, self.openstack)] |
705 | |
706 | def _get_openstack_release_string(self): |
707 | @@ -138,6 +141,7 @@ |
708 | ('trusty', 'icehouse'), |
709 | ('utopic', 'juno'), |
710 | ('vivid', 'kilo'), |
711 | + ('wily', 'liberty'), |
712 | ]) |
713 | if self.openstack: |
714 | os_origin = self.openstack.split(':')[1] |
715 | |
716 | === modified file 'tests/charmhelpers/contrib/openstack/amulet/utils.py' |
717 | --- tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-02-17 07:10:15 +0000 |
718 | +++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-06-19 16:15:27 +0000 |
719 | @@ -16,15 +16,15 @@ |
720 | |
721 | import logging |
722 | import os |
723 | +import six |
724 | import time |
725 | import urllib |
726 | |
727 | import glanceclient.v1.client as glance_client |
728 | +import heatclient.v1.client as heat_client |
729 | import keystoneclient.v2_0 as keystone_client |
730 | import novaclient.v1_1.client as nova_client |
731 | |
732 | -import six |
733 | - |
734 | from charmhelpers.contrib.amulet.utils import ( |
735 | AmuletUtils |
736 | ) |
737 | @@ -37,7 +37,7 @@ |
738 | """OpenStack amulet utilities. |
739 | |
740 | This class inherits from AmuletUtils and has additional support |
741 | - that is specifically for use by OpenStack charms. |
742 | + that is specifically for use by OpenStack charm tests. |
743 | """ |
744 | |
745 | def __init__(self, log_level=ERROR): |
746 | @@ -51,6 +51,8 @@ |
747 | Validate actual endpoint data vs expected endpoint data. The ports |
748 | are used to find the matching endpoint. |
749 | """ |
750 | + self.log.debug('Validating endpoint data...') |
751 | + self.log.debug('actual: {}'.format(repr(endpoints))) |
752 | found = False |
753 | for ep in endpoints: |
754 | self.log.debug('endpoint: {}'.format(repr(ep))) |
755 | @@ -77,6 +79,7 @@ |
756 | Validate a list of actual service catalog endpoints vs a list of |
757 | expected service catalog endpoints. |
758 | """ |
759 | + self.log.debug('Validating service catalog endpoint data...') |
760 | self.log.debug('actual: {}'.format(repr(actual))) |
761 | for k, v in six.iteritems(expected): |
762 | if k in actual: |
763 | @@ -93,6 +96,7 @@ |
764 | Validate a list of actual tenant data vs list of expected tenant |
765 | data. |
766 | """ |
767 | + self.log.debug('Validating tenant data...') |
768 | self.log.debug('actual: {}'.format(repr(actual))) |
769 | for e in expected: |
770 | found = False |
771 | @@ -114,6 +118,7 @@ |
772 | Validate a list of actual role data vs a list of expected role |
773 | data. |
774 | """ |
775 | + self.log.debug('Validating role data...') |
776 | self.log.debug('actual: {}'.format(repr(actual))) |
777 | for e in expected: |
778 | found = False |
779 | @@ -134,6 +139,7 @@ |
780 | Validate a list of actual user data vs a list of expected user |
781 | data. |
782 | """ |
783 | + self.log.debug('Validating user data...') |
784 | self.log.debug('actual: {}'.format(repr(actual))) |
785 | for e in expected: |
786 | found = False |
787 | @@ -155,17 +161,20 @@ |
788 | |
789 | Validate a list of actual flavors vs a list of expected flavors. |
790 | """ |
791 | + self.log.debug('Validating flavor data...') |
792 | self.log.debug('actual: {}'.format(repr(actual))) |
793 | act = [a.name for a in actual] |
794 | return self._validate_list_data(expected, act) |
795 | |
796 | def tenant_exists(self, keystone, tenant): |
797 | """Return True if tenant exists.""" |
798 | + self.log.debug('Checking if tenant exists ({})...'.format(tenant)) |
799 | return tenant in [t.name for t in keystone.tenants.list()] |
800 | |
801 | def authenticate_keystone_admin(self, keystone_sentry, user, password, |
802 | tenant): |
803 | """Authenticates admin user with the keystone admin endpoint.""" |
804 | + self.log.debug('Authenticating keystone admin...') |
805 | unit = keystone_sentry |
806 | service_ip = unit.relation('shared-db', |
807 | 'mysql:shared-db')['private-address'] |
808 | @@ -175,6 +184,7 @@ |
809 | |
810 | def authenticate_keystone_user(self, keystone, user, password, tenant): |
811 | """Authenticates a regular user with the keystone public endpoint.""" |
812 | + self.log.debug('Authenticating keystone user ({})...'.format(user)) |
813 | ep = keystone.service_catalog.url_for(service_type='identity', |
814 | endpoint_type='publicURL') |
815 | return keystone_client.Client(username=user, password=password, |
816 | @@ -182,12 +192,21 @@ |
817 | |
818 | def authenticate_glance_admin(self, keystone): |
819 | """Authenticates admin user with glance.""" |
820 | + self.log.debug('Authenticating glance admin...') |
821 | ep = keystone.service_catalog.url_for(service_type='image', |
822 | endpoint_type='adminURL') |
823 | return glance_client.Client(ep, token=keystone.auth_token) |
824 | |
825 | + def authenticate_heat_admin(self, keystone): |
826 | + """Authenticates the admin user with heat.""" |
827 | + self.log.debug('Authenticating heat admin...') |
828 | + ep = keystone.service_catalog.url_for(service_type='orchestration', |
829 | + endpoint_type='publicURL') |
830 | + return heat_client.Client(endpoint=ep, token=keystone.auth_token) |
831 | + |
832 | def authenticate_nova_user(self, keystone, user, password, tenant): |
833 | """Authenticates a regular user with nova-api.""" |
834 | + self.log.debug('Authenticating nova user ({})...'.format(user)) |
835 | ep = keystone.service_catalog.url_for(service_type='identity', |
836 | endpoint_type='publicURL') |
837 | return nova_client.Client(username=user, api_key=password, |
838 | @@ -195,6 +214,7 @@ |
839 | |
840 | def create_cirros_image(self, glance, image_name): |
841 | """Download the latest cirros image and upload it to glance.""" |
842 | + self.log.debug('Creating glance image ({})...'.format(image_name)) |
843 | http_proxy = os.getenv('AMULET_HTTP_PROXY') |
844 | self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy)) |
845 | if http_proxy: |
846 | @@ -235,6 +255,11 @@ |
847 | |
848 | def delete_image(self, glance, image): |
849 | """Delete the specified image.""" |
850 | + |
851 | + # /!\ DEPRECATION WARNING |
852 | + self.log.warn('/!\\ DEPRECATION WARNING: use ' |
853 | + 'delete_resource instead of delete_image.') |
854 | + self.log.debug('Deleting glance image ({})...'.format(image)) |
855 | num_before = len(list(glance.images.list())) |
856 | glance.images.delete(image) |
857 | |
858 | @@ -254,6 +279,8 @@ |
859 | |
860 | def create_instance(self, nova, image_name, instance_name, flavor): |
861 | """Create the specified instance.""" |
862 | + self.log.debug('Creating instance ' |
863 | + '({}|{}|{})'.format(instance_name, image_name, flavor)) |
864 | image = nova.images.find(name=image_name) |
865 | flavor = nova.flavors.find(name=flavor) |
866 | instance = nova.servers.create(name=instance_name, image=image, |
867 | @@ -276,6 +303,11 @@ |
868 | |
869 | def delete_instance(self, nova, instance): |
870 | """Delete the specified instance.""" |
871 | + |
872 | + # /!\ DEPRECATION WARNING |
873 | + self.log.warn('/!\\ DEPRECATION WARNING: use ' |
874 | + 'delete_resource instead of delete_instance.') |
875 | + self.log.debug('Deleting instance ({})...'.format(instance)) |
876 | num_before = len(list(nova.servers.list())) |
877 | nova.servers.delete(instance) |
878 | |
879 | @@ -292,3 +324,90 @@ |
880 | return False |
881 | |
882 | return True |
883 | + |
884 | + def create_or_get_keypair(self, nova, keypair_name="testkey"): |
885 | + """Create a new keypair, or return pointer if it already exists.""" |
886 | + try: |
887 | + _keypair = nova.keypairs.get(keypair_name) |
888 | + self.log.debug('Keypair ({}) already exists, ' |
889 | + 'using it.'.format(keypair_name)) |
890 | + return _keypair |
891 | + except: |
892 | + self.log.debug('Keypair ({}) does not exist, ' |
893 | + 'creating it.'.format(keypair_name)) |
894 | + |
895 | + _keypair = nova.keypairs.create(name=keypair_name) |
896 | + return _keypair |
897 | + |
898 | + def delete_resource(self, resource, resource_id, |
899 | + msg="resource", max_wait=120): |
900 | + """Delete one openstack resource, such as one instance, keypair, |
901 | + image, volume, stack, etc., and confirm deletion within max wait time. |
902 | + |
903 | + :param resource: pointer to os resource type, ex:glance_client.images |
904 | + :param resource_id: unique name or id for the openstack resource |
905 | + :param msg: text to identify purpose in logging |
906 | + :param max_wait: maximum wait time in seconds |
907 | + :returns: True if successful, otherwise False |
908 | + """ |
909 | + num_before = len(list(resource.list())) |
910 | + resource.delete(resource_id) |
911 | + |
912 | + tries = 0 |
913 | + num_after = len(list(resource.list())) |
914 | + while num_after != (num_before - 1) and tries < (max_wait / 4): |
915 | + self.log.debug('{} delete check: ' |
916 | + '{} [{}:{}] {}'.format(msg, tries, |
917 | + num_before, |
918 | + num_after, |
919 | + resource_id)) |
920 | + time.sleep(4) |
921 | + num_after = len(list(resource.list())) |
922 | + tries += 1 |
923 | + |
924 | + self.log.debug('{}: expected, actual count = {}, ' |
925 | + '{}'.format(msg, num_before - 1, num_after)) |
926 | + |
927 | + if num_after == (num_before - 1): |
928 | + return True |
929 | + else: |
930 | + self.log.error('{} delete timed out'.format(msg)) |
931 | + return False |
932 | + |
933 | + def resource_reaches_status(self, resource, resource_id, |
934 | + expected_stat='available', |
935 | + msg='resource', max_wait=120): |
936 | + """Wait for an openstack resources status to reach an |
937 | + expected status within a specified time. Useful to confirm that |
938 | + nova instances, cinder vols, snapshots, glance images, heat stacks |
939 | + and other resources eventually reach the expected status. |
940 | + |
941 | + :param resource: pointer to os resource type, ex: heat_client.stacks |
942 | + :param resource_id: unique id for the openstack resource |
943 | + :param expected_stat: status to expect resource to reach |
944 | + :param msg: text to identify purpose in logging |
945 | + :param max_wait: maximum wait time in seconds |
946 | + :returns: True if successful, False if status is not reached |
947 | + """ |
948 | + |
949 | + tries = 0 |
950 | + resource_stat = resource.get(resource_id).status |
951 | + while resource_stat != expected_stat and tries < (max_wait / 4): |
952 | + self.log.debug('{} status check: ' |
953 | + '{} [{}:{}] {}'.format(msg, tries, |
954 | + resource_stat, |
955 | + expected_stat, |
956 | + resource_id)) |
957 | + time.sleep(4) |
958 | + resource_stat = resource.get(resource_id).status |
959 | + tries += 1 |
960 | + |
961 | + self.log.debug('{}: expected, actual status = {}, ' |
962 | + '{}'.format(msg, resource_stat, expected_stat)) |
963 | + |
964 | + if resource_stat == expected_stat: |
965 | + return True |
966 | + else: |
967 | + self.log.debug('{} never reached expected status: ' |
968 | + '{}'.format(resource_id, expected_stat)) |
969 | + return False |
charm_lint_check #5474 neutron-api-next for corey.bryant mp262471
LINT OK: passed
Build: http:// 10.245. 162.77: 8080/job/ charm_lint_ check/5474/