Merge lp:~corey.bryant/charms/trusty/nova-compute/sync-charm-helpers into lp:~openstack-charmers-archive/charms/trusty/nova-compute/next
- Trusty Tahr (14.04)
- sync-charm-helpers
- Merge into next
Proposed by
Corey Bryant
Status: | Merged |
---|---|
Merged at revision: | 74 |
Proposed branch: | lp:~corey.bryant/charms/trusty/nova-compute/sync-charm-helpers |
Merge into: | lp:~openstack-charmers-archive/charms/trusty/nova-compute/next |
Diff against target: |
696 lines (+273/-88) 7 files modified
hooks/charmhelpers/contrib/hahelpers/cluster.py (+9/-0) hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+13/-7) hooks/charmhelpers/contrib/openstack/amulet/utils.py (+86/-20) tests/charmhelpers/contrib/amulet/deployment.py (+20/-7) tests/charmhelpers/contrib/amulet/utils.py (+46/-27) tests/charmhelpers/contrib/openstack/amulet/deployment.py (+13/-7) tests/charmhelpers/contrib/openstack/amulet/utils.py (+86/-20) |
To merge this branch: | bzr merge lp:~corey.bryant/charms/trusty/nova-compute/sync-charm-helpers |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
OpenStack Charmers | Pending | ||
Review via email:
|
Commit message
Description of the change
The amulet tests are broken without this.
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 | === modified file 'hooks/charmhelpers/contrib/hahelpers/cluster.py' | |||
2 | --- hooks/charmhelpers/contrib/hahelpers/cluster.py 2014-07-28 14:38:51 +0000 | |||
3 | +++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2014-07-30 15:24:47 +0000 | |||
4 | @@ -62,6 +62,15 @@ | |||
5 | 62 | return peers | 62 | return peers |
6 | 63 | 63 | ||
7 | 64 | 64 | ||
8 | 65 | def peer_ips(peer_relation='cluster', addr_key='private-address'): | ||
9 | 66 | '''Return a dict of peers and their private-address''' | ||
10 | 67 | peers = {} | ||
11 | 68 | for r_id in relation_ids(peer_relation): | ||
12 | 69 | for unit in relation_list(r_id): | ||
13 | 70 | peers[unit] = relation_get(addr_key, rid=r_id, unit=unit) | ||
14 | 71 | return peers | ||
15 | 72 | |||
16 | 73 | |||
17 | 65 | def oldest_peer(peers): | 74 | def oldest_peer(peers): |
18 | 66 | local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1]) | 75 | local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1]) |
19 | 67 | for peer in peers: | 76 | for peer in peers: |
20 | 68 | 77 | ||
21 | === modified file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py' | |||
22 | --- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2014-07-28 11:36:16 +0000 | |||
23 | +++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2014-07-30 15:24:47 +0000 | |||
24 | @@ -4,8 +4,11 @@ | |||
25 | 4 | 4 | ||
26 | 5 | 5 | ||
27 | 6 | class OpenStackAmuletDeployment(AmuletDeployment): | 6 | class OpenStackAmuletDeployment(AmuletDeployment): |
30 | 7 | """This class inherits from AmuletDeployment and has additional support | 7 | """OpenStack amulet deployment. |
31 | 8 | that is specifically for use by OpenStack charms.""" | 8 | |
32 | 9 | This class inherits from AmuletDeployment and has additional support | ||
33 | 10 | that is specifically for use by OpenStack charms. | ||
34 | 11 | """ | ||
35 | 9 | 12 | ||
36 | 10 | def __init__(self, series=None, openstack=None, source=None): | 13 | def __init__(self, series=None, openstack=None, source=None): |
37 | 11 | """Initialize the deployment environment.""" | 14 | """Initialize the deployment environment.""" |
38 | @@ -40,11 +43,14 @@ | |||
39 | 40 | self.d.configure(service, config) | 43 | self.d.configure(service, config) |
40 | 41 | 44 | ||
41 | 42 | def _get_openstack_release(self): | 45 | def _get_openstack_release(self): |
47 | 43 | """Return an integer representing the enum value of the openstack | 46 | """Get openstack release. |
48 | 44 | release.""" | 47 | |
49 | 45 | self.precise_essex, self.precise_folsom, self.precise_grizzly, \ | 48 | Return an integer representing the enum value of the openstack |
50 | 46 | self.precise_havana, self.precise_icehouse, \ | 49 | release. |
51 | 47 | self.trusty_icehouse = range(6) | 50 | """ |
52 | 51 | (self.precise_essex, self.precise_folsom, self.precise_grizzly, | ||
53 | 52 | self.precise_havana, self.precise_icehouse, | ||
54 | 53 | self.trusty_icehouse) = range(6) | ||
55 | 48 | releases = { | 54 | releases = { |
56 | 49 | ('precise', None): self.precise_essex, | 55 | ('precise', None): self.precise_essex, |
57 | 50 | ('precise', 'cloud:precise-folsom'): self.precise_folsom, | 56 | ('precise', 'cloud:precise-folsom'): self.precise_folsom, |
58 | 51 | 57 | ||
59 | === modified file 'hooks/charmhelpers/contrib/openstack/amulet/utils.py' | |||
60 | --- hooks/charmhelpers/contrib/openstack/amulet/utils.py 2014-07-28 11:36:16 +0000 | |||
61 | +++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2014-07-30 15:24:47 +0000 | |||
62 | @@ -16,8 +16,11 @@ | |||
63 | 16 | 16 | ||
64 | 17 | 17 | ||
65 | 18 | class OpenStackAmuletUtils(AmuletUtils): | 18 | class OpenStackAmuletUtils(AmuletUtils): |
68 | 19 | """This class inherits from AmuletUtils and has additional support | 19 | """OpenStack amulet utilities. |
69 | 20 | that is specifically for use by OpenStack charms.""" | 20 | |
70 | 21 | This class inherits from AmuletUtils and has additional support | ||
71 | 22 | that is specifically for use by OpenStack charms. | ||
72 | 23 | """ | ||
73 | 21 | 24 | ||
74 | 22 | def __init__(self, log_level=ERROR): | 25 | def __init__(self, log_level=ERROR): |
75 | 23 | """Initialize the deployment environment.""" | 26 | """Initialize the deployment environment.""" |
76 | @@ -25,13 +28,17 @@ | |||
77 | 25 | 28 | ||
78 | 26 | def validate_endpoint_data(self, endpoints, admin_port, internal_port, | 29 | def validate_endpoint_data(self, endpoints, admin_port, internal_port, |
79 | 27 | public_port, expected): | 30 | public_port, expected): |
82 | 28 | """Validate actual endpoint data vs expected endpoint data. The ports | 31 | """Validate endpoint data. |
83 | 29 | are used to find the matching endpoint.""" | 32 | |
84 | 33 | Validate actual endpoint data vs expected endpoint data. The ports | ||
85 | 34 | are used to find the matching endpoint. | ||
86 | 35 | """ | ||
87 | 30 | found = False | 36 | found = False |
88 | 31 | for ep in endpoints: | 37 | for ep in endpoints: |
89 | 32 | self.log.debug('endpoint: {}'.format(repr(ep))) | 38 | self.log.debug('endpoint: {}'.format(repr(ep))) |
92 | 33 | if admin_port in ep.adminurl and internal_port in ep.internalurl \ | 39 | if (admin_port in ep.adminurl and |
93 | 34 | and public_port in ep.publicurl: | 40 | internal_port in ep.internalurl and |
94 | 41 | public_port in ep.publicurl): | ||
95 | 35 | found = True | 42 | found = True |
96 | 36 | actual = {'id': ep.id, | 43 | actual = {'id': ep.id, |
97 | 37 | 'region': ep.region, | 44 | 'region': ep.region, |
98 | @@ -47,8 +54,11 @@ | |||
99 | 47 | return 'endpoint not found' | 54 | return 'endpoint not found' |
100 | 48 | 55 | ||
101 | 49 | def validate_svc_catalog_endpoint_data(self, expected, actual): | 56 | def validate_svc_catalog_endpoint_data(self, expected, actual): |
104 | 50 | """Validate a list of actual service catalog endpoints vs a list of | 57 | """Validate service catalog endpoint data. |
105 | 51 | expected service catalog endpoints.""" | 58 | |
106 | 59 | Validate a list of actual service catalog endpoints vs a list of | ||
107 | 60 | expected service catalog endpoints. | ||
108 | 61 | """ | ||
109 | 52 | self.log.debug('actual: {}'.format(repr(actual))) | 62 | self.log.debug('actual: {}'.format(repr(actual))) |
110 | 53 | for k, v in expected.iteritems(): | 63 | for k, v in expected.iteritems(): |
111 | 54 | if k in actual: | 64 | if k in actual: |
112 | @@ -60,8 +70,11 @@ | |||
113 | 60 | return ret | 70 | return ret |
114 | 61 | 71 | ||
115 | 62 | def validate_tenant_data(self, expected, actual): | 72 | def validate_tenant_data(self, expected, actual): |
118 | 63 | """Validate a list of actual tenant data vs list of expected tenant | 73 | """Validate tenant data. |
119 | 64 | data.""" | 74 | |
120 | 75 | Validate a list of actual tenant data vs list of expected tenant | ||
121 | 76 | data. | ||
122 | 77 | """ | ||
123 | 65 | self.log.debug('actual: {}'.format(repr(actual))) | 78 | self.log.debug('actual: {}'.format(repr(actual))) |
124 | 66 | for e in expected: | 79 | for e in expected: |
125 | 67 | found = False | 80 | found = False |
126 | @@ -78,8 +91,11 @@ | |||
127 | 78 | return ret | 91 | return ret |
128 | 79 | 92 | ||
129 | 80 | def validate_role_data(self, expected, actual): | 93 | def validate_role_data(self, expected, actual): |
132 | 81 | """Validate a list of actual role data vs a list of expected role | 94 | """Validate role data. |
133 | 82 | data.""" | 95 | |
134 | 96 | Validate a list of actual role data vs a list of expected role | ||
135 | 97 | data. | ||
136 | 98 | """ | ||
137 | 83 | self.log.debug('actual: {}'.format(repr(actual))) | 99 | self.log.debug('actual: {}'.format(repr(actual))) |
138 | 84 | for e in expected: | 100 | for e in expected: |
139 | 85 | found = False | 101 | found = False |
140 | @@ -95,8 +111,11 @@ | |||
141 | 95 | return ret | 111 | return ret |
142 | 96 | 112 | ||
143 | 97 | def validate_user_data(self, expected, actual): | 113 | def validate_user_data(self, expected, actual): |
146 | 98 | """Validate a list of actual user data vs a list of expected user | 114 | """Validate user data. |
147 | 99 | data.""" | 115 | |
148 | 116 | Validate a list of actual user data vs a list of expected user | ||
149 | 117 | data. | ||
150 | 118 | """ | ||
151 | 100 | self.log.debug('actual: {}'.format(repr(actual))) | 119 | self.log.debug('actual: {}'.format(repr(actual))) |
152 | 101 | for e in expected: | 120 | for e in expected: |
153 | 102 | found = False | 121 | found = False |
154 | @@ -114,21 +133,24 @@ | |||
155 | 114 | return ret | 133 | return ret |
156 | 115 | 134 | ||
157 | 116 | def validate_flavor_data(self, expected, actual): | 135 | def validate_flavor_data(self, expected, actual): |
159 | 117 | """Validate a list of actual flavors vs a list of expected flavors.""" | 136 | """Validate flavor data. |
160 | 137 | |||
161 | 138 | Validate a list of actual flavors vs a list of expected flavors. | ||
162 | 139 | """ | ||
163 | 118 | self.log.debug('actual: {}'.format(repr(actual))) | 140 | self.log.debug('actual: {}'.format(repr(actual))) |
164 | 119 | act = [a.name for a in actual] | 141 | act = [a.name for a in actual] |
165 | 120 | return self._validate_list_data(expected, act) | 142 | return self._validate_list_data(expected, act) |
166 | 121 | 143 | ||
167 | 122 | def tenant_exists(self, keystone, tenant): | 144 | def tenant_exists(self, keystone, tenant): |
169 | 123 | """Return True if tenant exists""" | 145 | """Return True if tenant exists.""" |
170 | 124 | return tenant in [t.name for t in keystone.tenants.list()] | 146 | return tenant in [t.name for t in keystone.tenants.list()] |
171 | 125 | 147 | ||
172 | 126 | def authenticate_keystone_admin(self, keystone_sentry, user, password, | 148 | def authenticate_keystone_admin(self, keystone_sentry, user, password, |
173 | 127 | tenant): | 149 | tenant): |
174 | 128 | """Authenticates admin user with the keystone admin endpoint.""" | 150 | """Authenticates admin user with the keystone admin endpoint.""" |
178 | 129 | service_ip = \ | 151 | unit = keystone_sentry |
179 | 130 | keystone_sentry.relation('shared-db', | 152 | service_ip = unit.relation('shared-db', |
180 | 131 | 'mysql:shared-db')['private-address'] | 153 | 'mysql:shared-db')['private-address'] |
181 | 132 | ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8')) | 154 | ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8')) |
182 | 133 | return keystone_client.Client(username=user, password=password, | 155 | return keystone_client.Client(username=user, password=password, |
183 | 134 | tenant_name=tenant, auth_url=ep) | 156 | tenant_name=tenant, auth_url=ep) |
184 | @@ -177,12 +199,40 @@ | |||
185 | 177 | image = glance.images.create(name=image_name, is_public=True, | 199 | image = glance.images.create(name=image_name, is_public=True, |
186 | 178 | disk_format='qcow2', | 200 | disk_format='qcow2', |
187 | 179 | container_format='bare', data=f) | 201 | container_format='bare', data=f) |
188 | 202 | count = 1 | ||
189 | 203 | status = image.status | ||
190 | 204 | while status != 'active' and count < 10: | ||
191 | 205 | time.sleep(3) | ||
192 | 206 | image = glance.images.get(image.id) | ||
193 | 207 | status = image.status | ||
194 | 208 | self.log.debug('image status: {}'.format(status)) | ||
195 | 209 | count += 1 | ||
196 | 210 | |||
197 | 211 | if status != 'active': | ||
198 | 212 | self.log.error('image creation timed out') | ||
199 | 213 | return None | ||
200 | 214 | |||
201 | 180 | return image | 215 | return image |
202 | 181 | 216 | ||
203 | 182 | def delete_image(self, glance, image): | 217 | def delete_image(self, glance, image): |
204 | 183 | """Delete the specified image.""" | 218 | """Delete the specified image.""" |
205 | 219 | num_before = len(list(glance.images.list())) | ||
206 | 184 | glance.images.delete(image) | 220 | glance.images.delete(image) |
207 | 185 | 221 | ||
208 | 222 | count = 1 | ||
209 | 223 | num_after = len(list(glance.images.list())) | ||
210 | 224 | while num_after != (num_before - 1) and count < 10: | ||
211 | 225 | time.sleep(3) | ||
212 | 226 | num_after = len(list(glance.images.list())) | ||
213 | 227 | self.log.debug('number of images: {}'.format(num_after)) | ||
214 | 228 | count += 1 | ||
215 | 229 | |||
216 | 230 | if num_after != (num_before - 1): | ||
217 | 231 | self.log.error('image deletion timed out') | ||
218 | 232 | return False | ||
219 | 233 | |||
220 | 234 | return True | ||
221 | 235 | |||
222 | 186 | def create_instance(self, nova, image_name, instance_name, flavor): | 236 | def create_instance(self, nova, image_name, instance_name, flavor): |
223 | 187 | """Create the specified instance.""" | 237 | """Create the specified instance.""" |
224 | 188 | image = nova.images.find(name=image_name) | 238 | image = nova.images.find(name=image_name) |
225 | @@ -199,11 +249,27 @@ | |||
226 | 199 | self.log.debug('instance status: {}'.format(status)) | 249 | self.log.debug('instance status: {}'.format(status)) |
227 | 200 | count += 1 | 250 | count += 1 |
228 | 201 | 251 | ||
230 | 202 | if status == 'BUILD': | 252 | if status != 'ACTIVE': |
231 | 253 | self.log.error('instance creation timed out') | ||
232 | 203 | return None | 254 | return None |
233 | 204 | 255 | ||
234 | 205 | return instance | 256 | return instance |
235 | 206 | 257 | ||
236 | 207 | def delete_instance(self, nova, instance): | 258 | def delete_instance(self, nova, instance): |
237 | 208 | """Delete the specified instance.""" | 259 | """Delete the specified instance.""" |
238 | 260 | num_before = len(list(nova.servers.list())) | ||
239 | 209 | nova.servers.delete(instance) | 261 | nova.servers.delete(instance) |
240 | 262 | |||
241 | 263 | count = 1 | ||
242 | 264 | num_after = len(list(nova.servers.list())) | ||
243 | 265 | while num_after != (num_before - 1) and count < 10: | ||
244 | 266 | time.sleep(3) | ||
245 | 267 | num_after = len(list(nova.servers.list())) | ||
246 | 268 | self.log.debug('number of instances: {}'.format(num_after)) | ||
247 | 269 | count += 1 | ||
248 | 270 | |||
249 | 271 | if num_after != (num_before - 1): | ||
250 | 272 | self.log.error('instance deletion timed out') | ||
251 | 273 | return False | ||
252 | 274 | |||
253 | 275 | return True | ||
254 | 210 | 276 | ||
255 | === modified file 'tests/charmhelpers/contrib/amulet/deployment.py' | |||
256 | --- tests/charmhelpers/contrib/amulet/deployment.py 2014-07-28 11:36:16 +0000 | |||
257 | +++ tests/charmhelpers/contrib/amulet/deployment.py 2014-07-30 15:24:47 +0000 | |||
258 | @@ -1,9 +1,14 @@ | |||
259 | 1 | import amulet | 1 | import amulet |
260 | 2 | 2 | ||
261 | 3 | import os | ||
262 | 4 | |||
263 | 3 | 5 | ||
264 | 4 | class AmuletDeployment(object): | 6 | class AmuletDeployment(object): |
267 | 5 | """This class provides generic Amulet deployment and test runner | 7 | """Amulet deployment. |
268 | 6 | methods.""" | 8 | |
269 | 9 | This class provides generic Amulet deployment and test runner | ||
270 | 10 | methods. | ||
271 | 11 | """ | ||
272 | 7 | 12 | ||
273 | 8 | def __init__(self, series=None): | 13 | def __init__(self, series=None): |
274 | 9 | """Initialize the deployment environment.""" | 14 | """Initialize the deployment environment.""" |
275 | @@ -16,11 +21,19 @@ | |||
276 | 16 | self.d = amulet.Deployment() | 21 | self.d = amulet.Deployment() |
277 | 17 | 22 | ||
278 | 18 | def _add_services(self, this_service, other_services): | 23 | def _add_services(self, this_service, other_services): |
280 | 19 | """Add services to the deployment where this_service is the local charm | 24 | """Add services. |
281 | 25 | |||
282 | 26 | Add services to the deployment where this_service is the local charm | ||
283 | 20 | that we're focused on testing and other_services are the other | 27 | that we're focused on testing and other_services are the other |
285 | 21 | charms that come from the charm store.""" | 28 | charms that come from the charm store. |
286 | 29 | """ | ||
287 | 22 | name, units = range(2) | 30 | name, units = range(2) |
289 | 23 | self.this_service = this_service[name] | 31 | |
290 | 32 | if this_service[name] != os.path.basename(os.getcwd()): | ||
291 | 33 | s = this_service[name] | ||
292 | 34 | msg = "The charm's root directory name needs to be {}".format(s) | ||
293 | 35 | amulet.raise_status(amulet.FAIL, msg=msg) | ||
294 | 36 | |||
295 | 24 | self.d.add(this_service[name], units=this_service[units]) | 37 | self.d.add(this_service[name], units=this_service[units]) |
296 | 25 | 38 | ||
297 | 26 | for svc in other_services: | 39 | for svc in other_services: |
298 | @@ -45,10 +58,10 @@ | |||
299 | 45 | """Deploy environment and wait for all hooks to finish executing.""" | 58 | """Deploy environment and wait for all hooks to finish executing.""" |
300 | 46 | try: | 59 | try: |
301 | 47 | self.d.setup() | 60 | self.d.setup() |
303 | 48 | self.d.sentry.wait() | 61 | self.d.sentry.wait(timeout=900) |
304 | 49 | except amulet.helpers.TimeoutError: | 62 | except amulet.helpers.TimeoutError: |
305 | 50 | amulet.raise_status(amulet.FAIL, msg="Deployment timed out") | 63 | amulet.raise_status(amulet.FAIL, msg="Deployment timed out") |
307 | 51 | except: | 64 | except Exception: |
308 | 52 | raise | 65 | raise |
309 | 53 | 66 | ||
310 | 54 | def run_tests(self): | 67 | def run_tests(self): |
311 | 55 | 68 | ||
312 | === modified file 'tests/charmhelpers/contrib/amulet/utils.py' | |||
313 | --- tests/charmhelpers/contrib/amulet/utils.py 2014-07-28 11:36:16 +0000 | |||
314 | +++ tests/charmhelpers/contrib/amulet/utils.py 2014-07-30 15:24:47 +0000 | |||
315 | @@ -3,12 +3,15 @@ | |||
316 | 3 | import logging | 3 | import logging |
317 | 4 | import re | 4 | import re |
318 | 5 | import sys | 5 | import sys |
320 | 6 | from time import sleep | 6 | import time |
321 | 7 | 7 | ||
322 | 8 | 8 | ||
323 | 9 | class AmuletUtils(object): | 9 | class AmuletUtils(object): |
326 | 10 | """This class provides common utility functions that are used by Amulet | 10 | """Amulet utilities. |
327 | 11 | tests.""" | 11 | |
328 | 12 | This class provides common utility functions that are used by Amulet | ||
329 | 13 | tests. | ||
330 | 14 | """ | ||
331 | 12 | 15 | ||
332 | 13 | def __init__(self, log_level=logging.ERROR): | 16 | def __init__(self, log_level=logging.ERROR): |
333 | 14 | self.log = self.get_logger(level=log_level) | 17 | self.log = self.get_logger(level=log_level) |
334 | @@ -17,8 +20,8 @@ | |||
335 | 17 | """Get a logger object that will log to stdout.""" | 20 | """Get a logger object that will log to stdout.""" |
336 | 18 | log = logging | 21 | log = logging |
337 | 19 | logger = log.getLogger(name) | 22 | logger = log.getLogger(name) |
340 | 20 | fmt = \ | 23 | fmt = log.Formatter("%(asctime)s %(funcName)s " |
341 | 21 | log.Formatter("%(asctime)s %(funcName)s %(levelname)s: %(message)s") | 24 | "%(levelname)s: %(message)s") |
342 | 22 | 25 | ||
343 | 23 | handler = log.StreamHandler(stream=sys.stdout) | 26 | handler = log.StreamHandler(stream=sys.stdout) |
344 | 24 | handler.setLevel(level) | 27 | handler.setLevel(level) |
345 | @@ -38,7 +41,7 @@ | |||
346 | 38 | def valid_url(self, url): | 41 | def valid_url(self, url): |
347 | 39 | p = re.compile( | 42 | p = re.compile( |
348 | 40 | r'^(?:http|ftp)s?://' | 43 | r'^(?:http|ftp)s?://' |
350 | 41 | r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # flake8: noqa | 44 | r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # noqa |
351 | 42 | r'localhost|' | 45 | r'localhost|' |
352 | 43 | r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' | 46 | r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' |
353 | 44 | r'(?::\d+)?' | 47 | r'(?::\d+)?' |
354 | @@ -50,8 +53,11 @@ | |||
355 | 50 | return False | 53 | return False |
356 | 51 | 54 | ||
357 | 52 | def validate_services(self, commands): | 55 | def validate_services(self, commands): |
360 | 53 | """Verify the specified services are running on the corresponding | 56 | """Validate services. |
361 | 54 | service units.""" | 57 | |
362 | 58 | Verify the specified services are running on the corresponding | ||
363 | 59 | service units. | ||
364 | 60 | """ | ||
365 | 55 | for k, v in commands.iteritems(): | 61 | for k, v in commands.iteritems(): |
366 | 56 | for cmd in v: | 62 | for cmd in v: |
367 | 57 | output, code = k.run(cmd) | 63 | output, code = k.run(cmd) |
368 | @@ -66,9 +72,13 @@ | |||
369 | 66 | config.readfp(io.StringIO(file_contents)) | 72 | config.readfp(io.StringIO(file_contents)) |
370 | 67 | return config | 73 | return config |
371 | 68 | 74 | ||
375 | 69 | def validate_config_data(self, sentry_unit, config_file, section, expected): | 75 | def validate_config_data(self, sentry_unit, config_file, section, |
376 | 70 | """Verify that the specified section of the config file contains | 76 | expected): |
377 | 71 | the expected option key:value pairs.""" | 77 | """Validate config file data. |
378 | 78 | |||
379 | 79 | Verify that the specified section of the config file contains | ||
380 | 80 | the expected option key:value pairs. | ||
381 | 81 | """ | ||
382 | 72 | config = self._get_config(sentry_unit, config_file) | 82 | config = self._get_config(sentry_unit, config_file) |
383 | 73 | 83 | ||
384 | 74 | if section != 'DEFAULT' and not config.has_section(section): | 84 | if section != 'DEFAULT' and not config.has_section(section): |
385 | @@ -78,20 +88,23 @@ | |||
386 | 78 | if not config.has_option(section, k): | 88 | if not config.has_option(section, k): |
387 | 79 | return "section [{}] is missing option {}".format(section, k) | 89 | return "section [{}] is missing option {}".format(section, k) |
388 | 80 | if config.get(section, k) != expected[k]: | 90 | if config.get(section, k) != expected[k]: |
391 | 81 | return "section [{}] {}:{} != expected {}:{}".format(section, | 91 | return "section [{}] {}:{} != expected {}:{}".format( |
392 | 82 | k, config.get(section, k), k, expected[k]) | 92 | section, k, config.get(section, k), k, expected[k]) |
393 | 83 | return None | 93 | return None |
394 | 84 | 94 | ||
395 | 85 | def _validate_dict_data(self, expected, actual): | 95 | def _validate_dict_data(self, expected, actual): |
397 | 86 | """Compare expected dictionary data vs actual dictionary data. | 96 | """Validate dictionary data. |
398 | 97 | |||
399 | 98 | Compare expected dictionary data vs actual dictionary data. | ||
400 | 87 | The values in the 'expected' dictionary can be strings, bools, ints, | 99 | The values in the 'expected' dictionary can be strings, bools, ints, |
401 | 88 | longs, or can be a function that evaluate a variable and returns a | 100 | longs, or can be a function that evaluate a variable and returns a |
403 | 89 | bool.""" | 101 | bool. |
404 | 102 | """ | ||
405 | 90 | for k, v in expected.iteritems(): | 103 | for k, v in expected.iteritems(): |
406 | 91 | if k in actual: | 104 | if k in actual: |
410 | 92 | if isinstance(v, basestring) or \ | 105 | if (isinstance(v, basestring) or |
411 | 93 | isinstance(v, bool) or \ | 106 | isinstance(v, bool) or |
412 | 94 | isinstance(v, (int, long)): | 107 | isinstance(v, (int, long))): |
413 | 95 | if v != actual[k]: | 108 | if v != actual[k]: |
414 | 96 | return "{}:{}".format(k, actual[k]) | 109 | return "{}:{}".format(k, actual[k]) |
415 | 97 | elif not v(actual[k]): | 110 | elif not v(actual[k]): |
416 | @@ -114,7 +127,7 @@ | |||
417 | 114 | return None | 127 | return None |
418 | 115 | 128 | ||
419 | 116 | def not_null(self, string): | 129 | def not_null(self, string): |
421 | 117 | if string != None: | 130 | if string is not None: |
422 | 118 | return True | 131 | return True |
423 | 119 | else: | 132 | else: |
424 | 120 | return False | 133 | return False |
425 | @@ -128,9 +141,12 @@ | |||
426 | 128 | return sentry_unit.directory_stat(directory)['mtime'] | 141 | return sentry_unit.directory_stat(directory)['mtime'] |
427 | 129 | 142 | ||
428 | 130 | def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False): | 143 | def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False): |
430 | 131 | """Determine start time of the process based on the last modification | 144 | """Get process' start time. |
431 | 145 | |||
432 | 146 | Determine start time of the process based on the last modification | ||
433 | 132 | time of the /proc/pid directory. If pgrep_full is True, the process | 147 | time of the /proc/pid directory. If pgrep_full is True, the process |
435 | 133 | name is matched against the full command line.""" | 148 | name is matched against the full command line. |
436 | 149 | """ | ||
437 | 134 | if pgrep_full: | 150 | if pgrep_full: |
438 | 135 | cmd = 'pgrep -o -f {}'.format(service) | 151 | cmd = 'pgrep -o -f {}'.format(service) |
439 | 136 | else: | 152 | else: |
440 | @@ -139,13 +155,16 @@ | |||
441 | 139 | return self._get_dir_mtime(sentry_unit, proc_dir) | 155 | return self._get_dir_mtime(sentry_unit, proc_dir) |
442 | 140 | 156 | ||
443 | 141 | def service_restarted(self, sentry_unit, service, filename, | 157 | def service_restarted(self, sentry_unit, service, filename, |
446 | 142 | pgrep_full=False): | 158 | pgrep_full=False, sleep_time=20): |
447 | 143 | """Compare a service's start time vs a file's last modification time | 159 | """Check if service was restarted. |
448 | 160 | |||
449 | 161 | Compare a service's start time vs a file's last modification time | ||
450 | 144 | (such as a config file for that service) to determine if the service | 162 | (such as a config file for that service) to determine if the service |
455 | 145 | has been restarted.""" | 163 | has been restarted. |
456 | 146 | sleep(10) | 164 | """ |
457 | 147 | if self._get_proc_start_time(sentry_unit, service, pgrep_full) >= \ | 165 | time.sleep(sleep_time) |
458 | 148 | self._get_file_mtime(sentry_unit, filename): | 166 | if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >= |
459 | 167 | self._get_file_mtime(sentry_unit, filename)): | ||
460 | 149 | return True | 168 | return True |
461 | 150 | else: | 169 | else: |
462 | 151 | return False | 170 | return False |
463 | 152 | 171 | ||
464 | === modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py' | |||
465 | --- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2014-07-28 11:36:16 +0000 | |||
466 | +++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2014-07-30 15:24:47 +0000 | |||
467 | @@ -4,8 +4,11 @@ | |||
468 | 4 | 4 | ||
469 | 5 | 5 | ||
470 | 6 | class OpenStackAmuletDeployment(AmuletDeployment): | 6 | class OpenStackAmuletDeployment(AmuletDeployment): |
473 | 7 | """This class inherits from AmuletDeployment and has additional support | 7 | """OpenStack amulet deployment. |
474 | 8 | that is specifically for use by OpenStack charms.""" | 8 | |
475 | 9 | This class inherits from AmuletDeployment and has additional support | ||
476 | 10 | that is specifically for use by OpenStack charms. | ||
477 | 11 | """ | ||
478 | 9 | 12 | ||
479 | 10 | def __init__(self, series=None, openstack=None, source=None): | 13 | def __init__(self, series=None, openstack=None, source=None): |
480 | 11 | """Initialize the deployment environment.""" | 14 | """Initialize the deployment environment.""" |
481 | @@ -40,11 +43,14 @@ | |||
482 | 40 | self.d.configure(service, config) | 43 | self.d.configure(service, config) |
483 | 41 | 44 | ||
484 | 42 | def _get_openstack_release(self): | 45 | def _get_openstack_release(self): |
490 | 43 | """Return an integer representing the enum value of the openstack | 46 | """Get openstack release. |
491 | 44 | release.""" | 47 | |
492 | 45 | self.precise_essex, self.precise_folsom, self.precise_grizzly, \ | 48 | Return an integer representing the enum value of the openstack |
493 | 46 | self.precise_havana, self.precise_icehouse, \ | 49 | release. |
494 | 47 | self.trusty_icehouse = range(6) | 50 | """ |
495 | 51 | (self.precise_essex, self.precise_folsom, self.precise_grizzly, | ||
496 | 52 | self.precise_havana, self.precise_icehouse, | ||
497 | 53 | self.trusty_icehouse) = range(6) | ||
498 | 48 | releases = { | 54 | releases = { |
499 | 49 | ('precise', None): self.precise_essex, | 55 | ('precise', None): self.precise_essex, |
500 | 50 | ('precise', 'cloud:precise-folsom'): self.precise_folsom, | 56 | ('precise', 'cloud:precise-folsom'): self.precise_folsom, |
501 | 51 | 57 | ||
502 | === modified file 'tests/charmhelpers/contrib/openstack/amulet/utils.py' | |||
503 | --- tests/charmhelpers/contrib/openstack/amulet/utils.py 2014-07-28 11:36:16 +0000 | |||
504 | +++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2014-07-30 15:24:47 +0000 | |||
505 | @@ -16,8 +16,11 @@ | |||
506 | 16 | 16 | ||
507 | 17 | 17 | ||
508 | 18 | class OpenStackAmuletUtils(AmuletUtils): | 18 | class OpenStackAmuletUtils(AmuletUtils): |
511 | 19 | """This class inherits from AmuletUtils and has additional support | 19 | """OpenStack amulet utilities. |
512 | 20 | that is specifically for use by OpenStack charms.""" | 20 | |
513 | 21 | This class inherits from AmuletUtils and has additional support | ||
514 | 22 | that is specifically for use by OpenStack charms. | ||
515 | 23 | """ | ||
516 | 21 | 24 | ||
517 | 22 | def __init__(self, log_level=ERROR): | 25 | def __init__(self, log_level=ERROR): |
518 | 23 | """Initialize the deployment environment.""" | 26 | """Initialize the deployment environment.""" |
519 | @@ -25,13 +28,17 @@ | |||
520 | 25 | 28 | ||
521 | 26 | def validate_endpoint_data(self, endpoints, admin_port, internal_port, | 29 | def validate_endpoint_data(self, endpoints, admin_port, internal_port, |
522 | 27 | public_port, expected): | 30 | public_port, expected): |
525 | 28 | """Validate actual endpoint data vs expected endpoint data. The ports | 31 | """Validate endpoint data. |
526 | 29 | are used to find the matching endpoint.""" | 32 | |
527 | 33 | Validate actual endpoint data vs expected endpoint data. The ports | ||
528 | 34 | are used to find the matching endpoint. | ||
529 | 35 | """ | ||
530 | 30 | found = False | 36 | found = False |
531 | 31 | for ep in endpoints: | 37 | for ep in endpoints: |
532 | 32 | self.log.debug('endpoint: {}'.format(repr(ep))) | 38 | self.log.debug('endpoint: {}'.format(repr(ep))) |
535 | 33 | if admin_port in ep.adminurl and internal_port in ep.internalurl \ | 39 | if (admin_port in ep.adminurl and |
536 | 34 | and public_port in ep.publicurl: | 40 | internal_port in ep.internalurl and |
537 | 41 | public_port in ep.publicurl): | ||
538 | 35 | found = True | 42 | found = True |
539 | 36 | actual = {'id': ep.id, | 43 | actual = {'id': ep.id, |
540 | 37 | 'region': ep.region, | 44 | 'region': ep.region, |
541 | @@ -47,8 +54,11 @@ | |||
542 | 47 | return 'endpoint not found' | 54 | return 'endpoint not found' |
543 | 48 | 55 | ||
544 | 49 | def validate_svc_catalog_endpoint_data(self, expected, actual): | 56 | def validate_svc_catalog_endpoint_data(self, expected, actual): |
547 | 50 | """Validate a list of actual service catalog endpoints vs a list of | 57 | """Validate service catalog endpoint data. |
548 | 51 | expected service catalog endpoints.""" | 58 | |
549 | 59 | Validate a list of actual service catalog endpoints vs a list of | ||
550 | 60 | expected service catalog endpoints. | ||
551 | 61 | """ | ||
552 | 52 | self.log.debug('actual: {}'.format(repr(actual))) | 62 | self.log.debug('actual: {}'.format(repr(actual))) |
553 | 53 | for k, v in expected.iteritems(): | 63 | for k, v in expected.iteritems(): |
554 | 54 | if k in actual: | 64 | if k in actual: |
555 | @@ -60,8 +70,11 @@ | |||
556 | 60 | return ret | 70 | return ret |
557 | 61 | 71 | ||
558 | 62 | def validate_tenant_data(self, expected, actual): | 72 | def validate_tenant_data(self, expected, actual): |
561 | 63 | """Validate a list of actual tenant data vs list of expected tenant | 73 | """Validate tenant data. |
562 | 64 | data.""" | 74 | |
563 | 75 | Validate a list of actual tenant data vs list of expected tenant | ||
564 | 76 | data. | ||
565 | 77 | """ | ||
566 | 65 | self.log.debug('actual: {}'.format(repr(actual))) | 78 | self.log.debug('actual: {}'.format(repr(actual))) |
567 | 66 | for e in expected: | 79 | for e in expected: |
568 | 67 | found = False | 80 | found = False |
569 | @@ -78,8 +91,11 @@ | |||
570 | 78 | return ret | 91 | return ret |
571 | 79 | 92 | ||
572 | 80 | def validate_role_data(self, expected, actual): | 93 | def validate_role_data(self, expected, actual): |
575 | 81 | """Validate a list of actual role data vs a list of expected role | 94 | """Validate role data. |
576 | 82 | data.""" | 95 | |
577 | 96 | Validate a list of actual role data vs a list of expected role | ||
578 | 97 | data. | ||
579 | 98 | """ | ||
580 | 83 | self.log.debug('actual: {}'.format(repr(actual))) | 99 | self.log.debug('actual: {}'.format(repr(actual))) |
581 | 84 | for e in expected: | 100 | for e in expected: |
582 | 85 | found = False | 101 | found = False |
583 | @@ -95,8 +111,11 @@ | |||
584 | 95 | return ret | 111 | return ret |
585 | 96 | 112 | ||
586 | 97 | def validate_user_data(self, expected, actual): | 113 | def validate_user_data(self, expected, actual): |
589 | 98 | """Validate a list of actual user data vs a list of expected user | 114 | """Validate user data. |
590 | 99 | data.""" | 115 | |
591 | 116 | Validate a list of actual user data vs a list of expected user | ||
592 | 117 | data. | ||
593 | 118 | """ | ||
594 | 100 | self.log.debug('actual: {}'.format(repr(actual))) | 119 | self.log.debug('actual: {}'.format(repr(actual))) |
595 | 101 | for e in expected: | 120 | for e in expected: |
596 | 102 | found = False | 121 | found = False |
597 | @@ -114,21 +133,24 @@ | |||
598 | 114 | return ret | 133 | return ret |
599 | 115 | 134 | ||
600 | 116 | def validate_flavor_data(self, expected, actual): | 135 | def validate_flavor_data(self, expected, actual): |
602 | 117 | """Validate a list of actual flavors vs a list of expected flavors.""" | 136 | """Validate flavor data. |
603 | 137 | |||
604 | 138 | Validate a list of actual flavors vs a list of expected flavors. | ||
605 | 139 | """ | ||
606 | 118 | self.log.debug('actual: {}'.format(repr(actual))) | 140 | self.log.debug('actual: {}'.format(repr(actual))) |
607 | 119 | act = [a.name for a in actual] | 141 | act = [a.name for a in actual] |
608 | 120 | return self._validate_list_data(expected, act) | 142 | return self._validate_list_data(expected, act) |
609 | 121 | 143 | ||
610 | 122 | def tenant_exists(self, keystone, tenant): | 144 | def tenant_exists(self, keystone, tenant): |
612 | 123 | """Return True if tenant exists""" | 145 | """Return True if tenant exists.""" |
613 | 124 | return tenant in [t.name for t in keystone.tenants.list()] | 146 | return tenant in [t.name for t in keystone.tenants.list()] |
614 | 125 | 147 | ||
615 | 126 | def authenticate_keystone_admin(self, keystone_sentry, user, password, | 148 | def authenticate_keystone_admin(self, keystone_sentry, user, password, |
616 | 127 | tenant): | 149 | tenant): |
617 | 128 | """Authenticates admin user with the keystone admin endpoint.""" | 150 | """Authenticates admin user with the keystone admin endpoint.""" |
621 | 129 | service_ip = \ | 151 | unit = keystone_sentry |
622 | 130 | keystone_sentry.relation('shared-db', | 152 | service_ip = unit.relation('shared-db', |
623 | 131 | 'mysql:shared-db')['private-address'] | 153 | 'mysql:shared-db')['private-address'] |
624 | 132 | ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8')) | 154 | ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8')) |
625 | 133 | return keystone_client.Client(username=user, password=password, | 155 | return keystone_client.Client(username=user, password=password, |
626 | 134 | tenant_name=tenant, auth_url=ep) | 156 | tenant_name=tenant, auth_url=ep) |
627 | @@ -177,12 +199,40 @@ | |||
628 | 177 | image = glance.images.create(name=image_name, is_public=True, | 199 | image = glance.images.create(name=image_name, is_public=True, |
629 | 178 | disk_format='qcow2', | 200 | disk_format='qcow2', |
630 | 179 | container_format='bare', data=f) | 201 | container_format='bare', data=f) |
631 | 202 | count = 1 | ||
632 | 203 | status = image.status | ||
633 | 204 | while status != 'active' and count < 10: | ||
634 | 205 | time.sleep(3) | ||
635 | 206 | image = glance.images.get(image.id) | ||
636 | 207 | status = image.status | ||
637 | 208 | self.log.debug('image status: {}'.format(status)) | ||
638 | 209 | count += 1 | ||
639 | 210 | |||
640 | 211 | if status != 'active': | ||
641 | 212 | self.log.error('image creation timed out') | ||
642 | 213 | return None | ||
643 | 214 | |||
644 | 180 | return image | 215 | return image |
645 | 181 | 216 | ||
646 | 182 | def delete_image(self, glance, image): | 217 | def delete_image(self, glance, image): |
647 | 183 | """Delete the specified image.""" | 218 | """Delete the specified image.""" |
648 | 219 | num_before = len(list(glance.images.list())) | ||
649 | 184 | glance.images.delete(image) | 220 | glance.images.delete(image) |
650 | 185 | 221 | ||
651 | 222 | count = 1 | ||
652 | 223 | num_after = len(list(glance.images.list())) | ||
653 | 224 | while num_after != (num_before - 1) and count < 10: | ||
654 | 225 | time.sleep(3) | ||
655 | 226 | num_after = len(list(glance.images.list())) | ||
656 | 227 | self.log.debug('number of images: {}'.format(num_after)) | ||
657 | 228 | count += 1 | ||
658 | 229 | |||
659 | 230 | if num_after != (num_before - 1): | ||
660 | 231 | self.log.error('image deletion timed out') | ||
661 | 232 | return False | ||
662 | 233 | |||
663 | 234 | return True | ||
664 | 235 | |||
665 | 186 | def create_instance(self, nova, image_name, instance_name, flavor): | 236 | def create_instance(self, nova, image_name, instance_name, flavor): |
666 | 187 | """Create the specified instance.""" | 237 | """Create the specified instance.""" |
667 | 188 | image = nova.images.find(name=image_name) | 238 | image = nova.images.find(name=image_name) |
668 | @@ -199,11 +249,27 @@ | |||
669 | 199 | self.log.debug('instance status: {}'.format(status)) | 249 | self.log.debug('instance status: {}'.format(status)) |
670 | 200 | count += 1 | 250 | count += 1 |
671 | 201 | 251 | ||
673 | 202 | if status == 'BUILD': | 252 | if status != 'ACTIVE': |
674 | 253 | self.log.error('instance creation timed out') | ||
675 | 203 | return None | 254 | return None |
676 | 204 | 255 | ||
677 | 205 | return instance | 256 | return instance |
678 | 206 | 257 | ||
679 | 207 | def delete_instance(self, nova, instance): | 258 | def delete_instance(self, nova, instance): |
680 | 208 | """Delete the specified instance.""" | 259 | """Delete the specified instance.""" |
681 | 260 | num_before = len(list(nova.servers.list())) | ||
682 | 209 | nova.servers.delete(instance) | 261 | nova.servers.delete(instance) |
683 | 262 | |||
684 | 263 | count = 1 | ||
685 | 264 | num_after = len(list(nova.servers.list())) | ||
686 | 265 | while num_after != (num_before - 1) and count < 10: | ||
687 | 266 | time.sleep(3) | ||
688 | 267 | num_after = len(list(nova.servers.list())) | ||
689 | 268 | self.log.debug('number of instances: {}'.format(num_after)) | ||
690 | 269 | count += 1 | ||
691 | 270 | |||
692 | 271 | if num_after != (num_before - 1): | ||
693 | 272 | self.log.error('instance deletion timed out') | ||
694 | 273 | return False | ||
695 | 274 | |||
696 | 275 | return True |