Merge lp:~chad.smith/charms/precise/block-storage-broker/bsb-ec2-boto into lp:charms/block-storage-broker
- Precise Pangolin (12.04)
- bsb-ec2-boto
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | David Britton | ||||
Approved revision: | 66 | ||||
Merged at revision: | 55 | ||||
Proposed branch: | lp:~chad.smith/charms/precise/block-storage-broker/bsb-ec2-boto | ||||
Merge into: | lp:charms/block-storage-broker | ||||
Diff against target: |
1436 lines (+487/-421) 4 files modified
hooks/hooks.py (+12/-6) hooks/test_hooks.py (+46/-18) hooks/test_util.py (+327/-327) hooks/util.py (+102/-70) |
||||
To merge this branch: | bzr merge lp:~chad.smith/charms/precise/block-storage-broker/bsb-ec2-boto | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
David Britton (community) | Approve | ||
Fernando Correa Neto (community) | Approve | ||
Review via email: mp+231275@code.launchpad.net |
Commit message
Description of the change
This branch moves block-storage-
The reason for this change is to avoid significant incompatibilities in euca2ools libraries that have been introduced across euca2ools major releases. By consuming python-boto instead of internal euca2ools libraries, we access a more stable API supported by Amazon that will remain more stable across releases than internal euca libs. As written this code currently also has been tested on trusty and will be used as well as the trusty release of this charm.
This can be quickly tested on AWS either using precise or trusty by changing the postgresql-
- change all mention of precise to trusty
- change postgresql-
Test procedure is something like the following:
1. Create postgresql-
2. juju-bootstrap -e your-ec2-
# to deploy block-storage-
3. juju-deployer -c postgresql-
----- postgresql-
common:
services:
postgresql:
branch: lp:~charmers/charms/precise/postgresql/trunk
branch: lp:~chad.smith/charms/precise/block-storage-broker/bsb-ec2-boto
doit-no-volume:
inherits: common
series: precise
services:
storage:
branch: lp:~charmers/charms/precise/storage/trunk
relations:
- [postgresql, storage]
- [storage, block-storage-
doit-with-
inherits: common
series: precise
services:
storage:
branch: lp:~charmers/charms/precise/storage/trunk
relations:
- [postgresql, storage]
- [storage, block-storage-
- 60. By Chad Smith
-
mocker assert cloud-archive:
havana is not added on trusty or later - 61. By Chad Smith
-
fcorrea review comment. docstring update
Chad Smith (chad.smith) wrote : | # |
Good deal Fernando thanks. I'll fix those comments right here with the exception of the isolation of ec2 from nova comment. We can handle that as a separate bug.
Dean Henrichsmeyer (dean) wrote : | # |
Yeah, let's not do the re-factoring for now. I'd prefer to focus on product priorities and circle back to this another time.
- 62. By Chad Smith
-
add apt_install and add_source params to install hook for testing
- 63. By Chad Smith
-
pull ec2_url regex parsing and connect_to_region calls out of try/except block to simplify error handling
- 64. By Chad Smith
-
unit test updates to use add_source apt_update testing params. add unit test for installing python-boto for ec2 provider provider
- 65. By Chad Smith
-
unit test rename for whitebox testing
Chad Smith (chad.smith) wrote : | # |
Fernando, I addressed your review comments on this branch with the exception of the re-factor. If you can create a bug/card for that we can work it when we have time after audit/history tasks
Fernando Correa Neto (fcorrea) wrote : | # |
+1, Chad.
I filed a bug about the refactoring. Also, if you want to address the tests cleanup in a separate branch, I'm fine as it's really a cleanup.
- 66. By Chad Smith
-
consolidation of environment setup in a class method _set_environmen
t_vars
David Britton (dpb) wrote : | # |
Hi Chad -- Thanks for submitting this, I tested successfully on both precise and trusty, appreciate your attention to detail on it. Since this test already has unit tests will submit to both precise and trusty.
Preview Diff
1 | === modified file 'hooks/hooks.py' |
2 | --- hooks/hooks.py 2014-07-18 00:58:58 +0000 |
3 | +++ hooks/hooks.py 2014-08-22 13:42:09 +0000 |
4 | @@ -1,6 +1,7 @@ |
5 | #!/usr/bin/env python |
6 | # vim: et ai ts=4 sw=4: |
7 | |
8 | +from charmhelpers import fetch |
9 | from charmhelpers.core import hookenv |
10 | from charmhelpers.core.hookenv import ERROR, INFO |
11 | |
12 | @@ -77,18 +78,23 @@ |
13 | |
14 | |
15 | @hooks.hook() |
16 | -def install(): |
17 | - """Install required packages if not present""" |
18 | - from charmhelpers import fetch |
19 | +def install(apt_install=None, add_source=None): |
20 | + """Install required packages if not present.""" |
21 | + |
22 | + if apt_install is None: # for testing purposes |
23 | + apt_install = fetch.apt_install |
24 | + if add_source is None: # for testing purposes |
25 | + add_source = fetch.add_source |
26 | + |
27 | provider = hookenv.config("provider") |
28 | if provider == "nova": |
29 | required_packages = ["python-novaclient"] |
30 | if int(get_running_series()['release'].split(".")[0]) < 14: |
31 | - fetch.add_source("cloud-archive:havana") |
32 | + add_source("cloud-archive:havana") |
33 | elif provider == "ec2": |
34 | - required_packages = ["euca2ools"] |
35 | + required_packages = ["python-boto"] |
36 | fetch.apt_update(fatal=True) |
37 | - fetch.apt_install(required_packages, fatal=True) |
38 | + apt_install(required_packages, fatal=True) |
39 | |
40 | |
41 | @hooks.hook("block-storage-relation-departed") |
42 | |
43 | === modified file 'hooks/test_hooks.py' |
44 | --- hooks/test_hooks.py 2014-07-18 04:13:23 +0000 |
45 | +++ hooks/test_hooks.py 2014-08-22 13:42:09 +0000 |
46 | @@ -182,24 +182,27 @@ |
47 | self.mocker.replay() |
48 | hooks.config_changed() |
49 | |
50 | - def test_install_installs_novaclient(self): |
51 | + def test_install_installs_novaclient_without_cloud_archive(self): |
52 | """ |
53 | - L{install} will call C{fetch.add_source} to add a cloud repository and |
54 | - install the C{python-novaclient} package. |
55 | + On releases C{trusty} and later L{install} will install the |
56 | + python-novaclient package without installing the cloud-archive |
57 | + repository. |
58 | """ |
59 | get_running_series = self.mocker.replace(hooks.get_running_series) |
60 | get_running_series() |
61 | - self.mocker.result({'release': '19.04'}) # Not precise |
62 | + self.mocker.result({'release': '14.04'}) # Trusty series |
63 | add_source = self.mocker.replace(fetch.add_source) |
64 | - # We are testing that add_source is not called. |
65 | - # add_source("cloud-archive:havana") |
66 | + add_source("cloud-archive:havana") |
67 | + self.mocker.count(0) # Test we never called add_source |
68 | apt_update = self.mocker.replace(fetch.apt_update) |
69 | apt_update(fatal=True) |
70 | - apt_install = self.mocker.replace(fetch.apt_install) |
71 | - apt_install(["python-novaclient"], fatal=True) |
72 | self.mocker.replay() |
73 | |
74 | - hooks.install() |
75 | + def apt_install(packages, fatal): |
76 | + self.assertEqual(["python-novaclient"], packages) |
77 | + self.assertTrue(fatal) |
78 | + |
79 | + hooks.install(apt_install=apt_install, add_source=add_source) |
80 | |
81 | def test_precise_install_adds_apt_source_and_installs_novaclient(self): |
82 | """ |
83 | @@ -209,15 +212,40 @@ |
84 | get_running_series = self.mocker.replace(hooks.get_running_series) |
85 | get_running_series() |
86 | self.mocker.result({'release': '12.04'}) # precise |
87 | - add_source = self.mocker.replace(fetch.add_source) |
88 | - add_source("cloud-archive:havana") # precise needs havana |
89 | - apt_update = self.mocker.replace(fetch.apt_update) |
90 | - apt_update(fatal=True) |
91 | - apt_install = self.mocker.replace(fetch.apt_install) |
92 | - apt_install(["python-novaclient"], fatal=True) |
93 | - self.mocker.replay() |
94 | - |
95 | - hooks.install() |
96 | + apt_update = self.mocker.replace(fetch.apt_update) |
97 | + apt_update(fatal=True) |
98 | + self.mocker.replay() |
99 | + |
100 | + def add_source(source): |
101 | + self.assertEqual("cloud-archive:havana", source) |
102 | + |
103 | + def apt_install(packages, fatal): |
104 | + self.assertEqual(["python-novaclient"], packages) |
105 | + self.assertTrue(fatal) |
106 | + |
107 | + hooks.install(apt_install=apt_install, add_source=add_source) |
108 | + |
109 | + def test_ec2_provider_install_installs_ec2_dependencies(self): |
110 | + """ |
111 | + When the provider is configured as C{ec2}, L{install} will install |
112 | + the C{python-boto} package. |
113 | + """ |
114 | + self.addCleanup( |
115 | + setattr, hooks.hookenv, "_config", hooks.hookenv._config) |
116 | + hooks.hookenv._config = ( |
117 | + ("key", ""), ("tenant", ""), ("provider", "ec2"), |
118 | + ("secret", ""), ("region", ""), |
119 | + ("endpoint", "")) |
120 | + |
121 | + apt_update = self.mocker.replace(fetch.apt_update) |
122 | + apt_update(fatal=True) |
123 | + self.mocker.replay() |
124 | + |
125 | + def apt_install(packages, fatal): |
126 | + self.assertEqual(["python-boto"], packages) |
127 | + self.assertTrue(fatal) |
128 | + |
129 | + hooks.install(apt_install=apt_install) |
130 | |
131 | def test_block_storage_relation_changed_waits_without_instance_id(self): |
132 | """ |
133 | |
134 | === modified file 'hooks/test_util.py' |
135 | --- hooks/test_util.py 2014-03-21 17:42:00 +0000 |
136 | +++ hooks/test_util.py 2014-08-22 13:42:09 +0000 |
137 | @@ -1,7 +1,8 @@ |
138 | import util |
139 | from util import StorageServiceUtil, ENVIRONMENT_MAP, generate_volume_label |
140 | import mocker |
141 | -import os |
142 | +from boto.exception import NoAuthHandlerFound, EC2ResponseError |
143 | +from socket import gaierror |
144 | import subprocess |
145 | from testing import TestHookenv |
146 | |
147 | @@ -19,6 +20,10 @@ |
148 | util.log = util.hookenv.log |
149 | self.storage = StorageServiceUtil("nova") |
150 | |
151 | + def _set_environment_vars(self, environ={}): |
152 | + self.addCleanup(setattr, util.os, "environ", util.os.environ) |
153 | + util.os.environ = environ |
154 | + |
155 | def test_invalid_provier_config(self): |
156 | """When an invalid provider config is set and error is reported.""" |
157 | result = self.assertRaises(SystemExit, StorageServiceUtil, "ce2") |
158 | @@ -42,8 +47,7 @@ |
159 | variables and then call L{validate_credentials} to assert |
160 | that environment variables provided give access to the service. |
161 | """ |
162 | - self.addCleanup(setattr, util.os, "environ", util.os.environ) |
163 | - util.os.environ = {} |
164 | + self._set_environment_vars({}) |
165 | |
166 | def mock_validate(): |
167 | pass |
168 | @@ -64,7 +68,7 @@ |
169 | L{load_environment} will exit in failure and log a message if any |
170 | required configuration option is not set. |
171 | """ |
172 | - self.addCleanup(setattr, util.os, "environ", util.os.environ) |
173 | + self._set_environment_vars({}) |
174 | |
175 | def mock_validate(): |
176 | raise SystemExit("something invalid") |
177 | @@ -89,7 +93,7 @@ |
178 | SystemExit, self.storage.validate_credentials) |
179 | self.assertEqual(result.code, 1) |
180 | message = ( |
181 | - "ERROR: Charm configured credentials can't access endpoint. " |
182 | + "ERROR: Charm configured credentials can't access nova endpoint. " |
183 | "Command '%s' returned non-zero exit status 1" % command) |
184 | self.assertIn( |
185 | message, util.hookenv._log_ERROR, "Not logged- %s" % message) |
186 | @@ -239,9 +243,7 @@ |
187 | with the os.environ[JUJU_REMOTE_UNIT]. |
188 | """ |
189 | unit_name = "postgresql/0" |
190 | - self.addCleanup( |
191 | - setattr, os, "environ", os.environ) |
192 | - os.environ = {"JUJU_REMOTE_UNIT": unit_name} |
193 | + self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name}) |
194 | volume_id = "123134124-1241412-1242141" |
195 | |
196 | def mock_describe(): |
197 | @@ -259,9 +261,7 @@ |
198 | for the os.environ[JUJU_REMOTE_UNIT]. |
199 | """ |
200 | unit_name = "postgresql/0" |
201 | - self.addCleanup( |
202 | - setattr, os, "environ", os.environ) |
203 | - os.environ = {"JUJU_REMOTE_UNIT": unit_name} |
204 | + self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name}) |
205 | |
206 | def mock_describe(val): |
207 | self.assertIsNone(val) |
208 | @@ -280,8 +280,7 @@ |
209 | multiple results the function exits with an error. |
210 | """ |
211 | unit_name = "postgresql/0" |
212 | - self.addCleanup(setattr, os, "environ", os.environ) |
213 | - os.environ = {"JUJU_REMOTE_UNIT": unit_name} |
214 | + self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name}) |
215 | |
216 | def mock_describe(): |
217 | return {"123123-123123": |
218 | @@ -306,8 +305,7 @@ |
219 | unit_name = "postgresql/0" |
220 | instance_id = "i-123123" |
221 | volume_id = "123-123-123" |
222 | - self.addCleanup(setattr, os, "environ", os.environ) |
223 | - os.environ = {"JUJU_REMOTE_UNIT": unit_name} |
224 | + self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name}) |
225 | |
226 | self.storage.load_environment = lambda: None |
227 | self.storage.describe_volumes = lambda volume_id: {} |
228 | @@ -331,8 +329,7 @@ |
229 | volume_id = "123-123-123" |
230 | instance_id = "i-123123123" |
231 | volume_label = "%s unit volume" % unit_name |
232 | - self.addCleanup(setattr, os, "environ", os.environ) |
233 | - os.environ = {"JUJU_REMOTE_UNIT": unit_name} |
234 | + self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name}) |
235 | self.storage.load_environment = lambda: None |
236 | |
237 | def mock_get_volume_id(label): |
238 | @@ -360,8 +357,7 @@ |
239 | unit_name = "postgresql/0" |
240 | instance_id = "i-123123" |
241 | volume_id = "123-123-123" |
242 | - self.addCleanup(setattr, os, "environ", os.environ) |
243 | - os.environ = {"JUJU_REMOTE_UNIT": unit_name} |
244 | + self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name}) |
245 | |
246 | self.storage.load_environment = lambda: None |
247 | |
248 | @@ -385,14 +381,13 @@ |
249 | unit_name = "postgresql/0" |
250 | instance_id = "i-123123" |
251 | volume_id = "123-123-123" |
252 | - self.addCleanup(setattr, os, "environ", os.environ) |
253 | - os.environ = {"JUJU_REMOTE_UNIT": unit_name} |
254 | + self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name}) |
255 | |
256 | self.describe_count = 0 |
257 | |
258 | self.storage.load_environment = lambda: None |
259 | |
260 | - sleep = self.mocker.replace("util.sleep") |
261 | + sleep = self.mocker.replace("time.sleep") |
262 | sleep(5) |
263 | self.mocker.replay() |
264 | |
265 | @@ -423,8 +418,7 @@ |
266 | unit_name = "postgresql/0" |
267 | instance_id = "i-123123" |
268 | volume_id = "123-123-123" |
269 | - self.addCleanup(setattr, os, "environ", os.environ) |
270 | - os.environ = {"JUJU_REMOTE_UNIT": unit_name} |
271 | + self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name}) |
272 | |
273 | self.storage.load_environment = lambda: None |
274 | |
275 | @@ -452,8 +446,7 @@ |
276 | volume_id = "123-123-123" |
277 | volume_label = "%s unit volume" % unit_name |
278 | default_volume_size = util.hookenv.config("default_volume_size") |
279 | - self.addCleanup(setattr, os, "environ", os.environ) |
280 | - os.environ = {"JUJU_REMOTE_UNIT": unit_name} |
281 | + self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name}) |
282 | |
283 | self.storage.load_environment = lambda: None |
284 | self.storage.get_volume_id = lambda _: None |
285 | @@ -880,6 +873,19 @@ |
286 | message, util.hookenv._log_INFO, "Not logged- %s" % message) |
287 | |
288 | |
289 | +class MockBotoEC2Connection(object): |
290 | + """Mock all calls to python-boto's EC2Connection class.""" |
291 | + def __init__(self, get_volumes=None, get_instances=None, |
292 | + create_volume=None, create_tags=None, attach_volume=None, |
293 | + detach_volume=None): |
294 | + self.get_all_volumes = get_volumes |
295 | + self.get_all_instances = get_instances |
296 | + self.create_volume = create_volume |
297 | + self.attach_volume = attach_volume |
298 | + self.detach_volume = detach_volume |
299 | + self.create_tags = create_tags |
300 | + |
301 | + |
302 | class MockEucaCommand(object): |
303 | def __init__(self, result): |
304 | self.result = result |
305 | @@ -918,8 +924,8 @@ |
306 | |
307 | |
308 | class MockVolume(object): |
309 | - def __init__(self, vol_id, device, instance_id, zone, size, status, |
310 | - snapshot_id, tags): |
311 | + def __init__(self, vol_id, device=None, instance_id=None, zone=None, |
312 | + size=None, status=None, snapshot_id=None, tags=None): |
313 | self.id = vol_id |
314 | self.attach_data = MockAttachData(device, instance_id) |
315 | self.zone = zone |
316 | @@ -936,11 +942,31 @@ |
317 | self.maxDiff = None |
318 | util.hookenv = TestHookenv( |
319 | {"key": "ec2key", "secret": "ec2password", |
320 | - "endpoint": "https://ec2-region-url:443/v2.0/", |
321 | + "endpoint": "https://ec2-region-1.com", |
322 | "default_volume_size": 11}) |
323 | + util.EC2_BOTO_CONFIG_FILE = self.makeFile() |
324 | util.log = util.hookenv.log |
325 | self.storage = StorageServiceUtil("ec2") |
326 | |
327 | + def _set_environment_vars(self, environ={}): |
328 | + self.addCleanup(setattr, util.os, "environ", util.os.environ) |
329 | + util.os.environ = environ |
330 | + |
331 | + def test_wb_setup_boto_config(self): |
332 | + """ |
333 | + L{_setup_boto_config} writes C{EC2_BOTO_CONFIG_FILE} with the provided |
334 | + C{key} and C{secret} parameters. |
335 | + """ |
336 | + key = "my_ec2_key" |
337 | + secret = "my_secret_access_key" |
338 | + expected = ["[Credentials]", |
339 | + "aws_access_key_id = %s" % key, |
340 | + "aws_secret_access_key = %s" % secret] |
341 | + self.storage._setup_boto_config(key=key, secret=secret) |
342 | + with open(util.EC2_BOTO_CONFIG_FILE) as boto_config: |
343 | + lines = boto_config.read().splitlines() |
344 | + self.assertEqual(lines, expected) |
345 | + |
346 | def test_load_environment_with_ec2_variables(self): |
347 | """ |
348 | L{load_environment} will setup script environment variables for ec2 |
349 | @@ -948,8 +974,7 @@ |
350 | variables and then call L{validate_credentials} to assert |
351 | that environment variables provided give access to the service. |
352 | """ |
353 | - self.addCleanup(setattr, util.os, "environ", util.os.environ) |
354 | - util.os.environ = {} |
355 | + self._set_environment_vars({}) |
356 | |
357 | def mock_validate(): |
358 | pass |
359 | @@ -959,7 +984,7 @@ |
360 | expected = { |
361 | "EC2_ACCESS_KEY": "ec2key", |
362 | "EC2_SECRET_KEY": "ec2password", |
363 | - "EC2_URL": "https://ec2-region-url:443/v2.0/" |
364 | + "EC2_URL": "https://ec2-region-1.com" |
365 | } |
366 | self.assertEqual(util.os.environ, expected) |
367 | |
368 | @@ -968,45 +993,28 @@ |
369 | L{load_environment} will exit in failure and log a message if any |
370 | required configuration option is not set. |
371 | """ |
372 | - self.addCleanup(setattr, util.os, "environ", util.os.environ) |
373 | - |
374 | def mock_validate(): |
375 | raise SystemExit("something invalid") |
376 | self.storage.validate_credentials = mock_validate |
377 | |
378 | self.assertRaises(SystemExit, self.storage.load_environment) |
379 | |
380 | - def test_validate_credentials_failure(self): |
381 | - """ |
382 | - L{validate_credentials} will attempt a simple euca command to ensure |
383 | - the environment is properly configured to access the nova service. |
384 | - Upon failure to contact the nova service, L{validate_credentials} will |
385 | - exit in error and log a message. |
386 | - """ |
387 | - command = "euca-describe-instances" |
388 | - nova_cmd = self.mocker.replace(subprocess.check_call) |
389 | - nova_cmd(command, shell=True) |
390 | - self.mocker.throw(subprocess.CalledProcessError(1, command)) |
391 | - self.mocker.replay() |
392 | - |
393 | - result = self.assertRaises( |
394 | - SystemExit, self.storage.validate_credentials) |
395 | - self.assertEqual(result.code, 1) |
396 | - message = ( |
397 | - "ERROR: Charm configured credentials can't access endpoint. " |
398 | - "Command '%s' returned non-zero exit status 1" % command) |
399 | - self.assertIn( |
400 | - message, util.hookenv._log_ERROR, "Not logged- %s" % message) |
401 | - |
402 | def test_validate_credentials(self): |
403 | """ |
404 | - L{validate_credentials} will succeed when a simple euca command |
405 | - succeeds due to a properly configured environment based on the charm |
406 | - configuration options. |
407 | + L{validate_credentials} will succeed when a boto's L{get_all_volumes} |
408 | + succeeds using C{EC2_URL} environment variable from charm configuration |
409 | + options. |
410 | """ |
411 | - command = "euca-describe-instances" |
412 | - nova_cmd = self.mocker.replace(subprocess.check_call) |
413 | - nova_cmd(command, shell=True) |
414 | + ec2_url = "https://ec2.us-west-1.amazonaws.com" |
415 | + self._set_environment_vars({"EC2_URL": ec2_url}) |
416 | + connect = self.mocker.replace("boto.ec2.connect_to_region") |
417 | + connect("us-west-1") |
418 | + |
419 | + def get_volumes(): |
420 | + return [] |
421 | + |
422 | + self.mocker.result(MockBotoEC2Connection( |
423 | + get_volumes=get_volumes)) |
424 | self.mocker.replay() |
425 | |
426 | self.storage.validate_credentials() |
427 | @@ -1040,9 +1048,7 @@ |
428 | labelled with the os.environ[JUJU_REMOTE_UNIT]. |
429 | """ |
430 | unit_name = "postgresql/0" |
431 | - self.addCleanup( |
432 | - setattr, os, "environ", os.environ) |
433 | - os.environ = {"JUJU_REMOTE_UNIT": unit_name} |
434 | + self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name}) |
435 | volume_id = "123134124-1241412-1242141" |
436 | |
437 | def mock_describe(val): |
438 | @@ -1061,9 +1067,7 @@ |
439 | L{_ec2_describe_volumes} for the os.environ[JUJU_REMOTE_UNIT]. |
440 | """ |
441 | unit_name = "postgresql/0" |
442 | - self.addCleanup( |
443 | - setattr, os, "environ", os.environ) |
444 | - os.environ = {"JUJU_REMOTE_UNIT": unit_name} |
445 | + self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name}) |
446 | |
447 | def mock_describe(val): |
448 | self.assertIsNone(val) |
449 | @@ -1082,8 +1086,7 @@ |
450 | multiple results the function exits with an error. |
451 | """ |
452 | unit_name = "postgresql/0" |
453 | - self.addCleanup(setattr, os, "environ", os.environ) |
454 | - os.environ = {"JUJU_REMOTE_UNIT": unit_name} |
455 | + self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name}) |
456 | |
457 | def mock_describe(val): |
458 | self.assertIsNone(val) |
459 | @@ -1109,8 +1112,7 @@ |
460 | unit_name = "postgresql/0" |
461 | instance_id = "i-123123" |
462 | volume_id = "123-123-123" |
463 | - self.addCleanup(setattr, os, "environ", os.environ) |
464 | - os.environ = {"JUJU_REMOTE_UNIT": unit_name} |
465 | + self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name}) |
466 | |
467 | self.storage.load_environment = lambda: None |
468 | self.storage._ec2_describe_volumes = lambda volume_id: {} |
469 | @@ -1134,8 +1136,7 @@ |
470 | volume_id = "123-123-123" |
471 | instance_id = "i-123123123" |
472 | volume_label = "%s unit volume" % unit_name |
473 | - self.addCleanup(setattr, os, "environ", os.environ) |
474 | - os.environ = {"JUJU_REMOTE_UNIT": unit_name} |
475 | + self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name}) |
476 | self.storage.load_environment = lambda: None |
477 | |
478 | def mock_get_volume_id(label): |
479 | @@ -1163,8 +1164,7 @@ |
480 | unit_name = "postgresql/0" |
481 | instance_id = "i-123123" |
482 | volume_id = "123-123-123" |
483 | - self.addCleanup(setattr, os, "environ", os.environ) |
484 | - os.environ = {"JUJU_REMOTE_UNIT": unit_name} |
485 | + self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name}) |
486 | |
487 | self.storage.load_environment = lambda: None |
488 | |
489 | @@ -1188,8 +1188,7 @@ |
490 | unit_name = "postgresql/0" |
491 | instance_id = "i-123123" |
492 | volume_id = "123-123-123" |
493 | - self.addCleanup(setattr, os, "environ", os.environ) |
494 | - os.environ = {"JUJU_REMOTE_UNIT": unit_name} |
495 | + self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name}) |
496 | |
497 | self.storage.load_environment = lambda: None |
498 | |
499 | @@ -1217,8 +1216,7 @@ |
500 | volume_id = "123-123-123" |
501 | volume_label = "%s unit volume" % unit_name |
502 | default_volume_size = util.hookenv.config("default_volume_size") |
503 | - self.addCleanup(setattr, os, "environ", os.environ) |
504 | - os.environ = {"JUJU_REMOTE_UNIT": unit_name} |
505 | + self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name}) |
506 | |
507 | self.storage.load_environment = lambda: None |
508 | self.storage.get_volume_id = lambda _: None |
509 | @@ -1240,26 +1238,116 @@ |
510 | self.assertIn( |
511 | message, util.hookenv._log_INFO, "Not logged- %s" % message) |
512 | |
513 | - def test_wb_ec2_describe_volumes_command_error(self): |
514 | - """ |
515 | - L{_ec2_describe_volumes} will exit in error when the euca2ools |
516 | - C{DescribeVolumes} command fails. |
517 | - """ |
518 | - euca_command = self.mocker.replace(self.storage.ec2_volume_class) |
519 | - euca_command() |
520 | - self.mocker.throw(SystemExit(1)) |
521 | - self.mocker.replay() |
522 | - |
523 | - result = self.assertRaises( |
524 | - SystemExit, self.storage._ec2_describe_volumes) |
525 | - self.assertEqual(result.code, 1) |
526 | - message = "ERROR: Couldn't contact EC2 using euca-describe-volumes" |
527 | + def test_wb_ec2_validate_credentials_invalid_ec2_url_environment_var(self): |
528 | + """ |
529 | + L{_ec2_validate_credentials} will exit in error when the C{EC2_URL} |
530 | + environment variable is invalid. |
531 | + """ |
532 | + self._set_environment_vars({"EC2_URL": "not-a-valid-ec2-url"}) |
533 | + |
534 | + result = self.assertRaises( |
535 | + SystemExit, self.storage._ec2_validate_credentials) |
536 | + self.assertEqual(result.code, 1) |
537 | + message = ( |
538 | + "ERROR: Couldn't get region from EC2_URL environment variable: " |
539 | + "not-a-valid-ec2-url.") |
540 | + self.assertIn( |
541 | + message, util.hookenv._log_ERROR, "Not logged- %s" % message) |
542 | + |
543 | + def test_wb_ec2_validate_credentials_absent_ec2_url_environment_var(self): |
544 | + """ |
545 | + L{_ec2_validate_credentials} will exit in error when the C{EC2_URL} |
546 | + environment variable is not present. |
547 | + """ |
548 | + self._set_environment_vars({}) |
549 | + |
550 | + result = self.assertRaises( |
551 | + SystemExit, self.storage._ec2_validate_credentials) |
552 | + self.assertEqual(result.code, 1) |
553 | + message = ( |
554 | + "ERROR: Couldn't get region from EC2_URL environment variable: " |
555 | + "NOT SET.") |
556 | + self.assertIn( |
557 | + message, util.hookenv._log_ERROR, "Not logged- %s" % message) |
558 | + |
559 | + def test_wb_ec2_validate_credentials_unavailable_credentials(self): |
560 | + """ |
561 | + L{_ec2_validate_credentials} will exit in error when the EC2 |
562 | + credentials are not present in /etc/boto.cfg. |
563 | + """ |
564 | + self._set_environment_vars( |
565 | + {"EC2_URL": "https://ec2.us-west-1.amazonaws.com"}) |
566 | + connect = self.mocker.replace("boto.ec2.connect_to_region") |
567 | + connect("us-west-1") |
568 | + |
569 | + def get_volumes_error(): |
570 | + raise NoAuthHandlerFound("No handler was ready to auth.") |
571 | + |
572 | + self.mocker.result( |
573 | + MockBotoEC2Connection(get_volumes=get_volumes_error)) |
574 | + self.mocker.replay() |
575 | + |
576 | + result = self.assertRaises( |
577 | + SystemExit, self.storage._ec2_validate_credentials) |
578 | + self.assertEqual(result.code, 1) |
579 | + message = ( |
580 | + "ERROR: EC2 credentials not found in /etc/boto.cfg. Cannot " |
581 | + "authenticate.") |
582 | + self.assertIn( |
583 | + message, util.hookenv._log_ERROR, "Not logged- %s" % message) |
584 | + |
585 | + def test_wb_ec2_validate_credentials_invalid_credentials(self): |
586 | + """ |
587 | + L{_ec2_validate_credentials} will exit in error when the EC2 |
588 | + credentials are not valid for the C{EC2_URL} provided. |
589 | + """ |
590 | + self._set_environment_vars( |
591 | + {"EC2_URL": "https://ec2.us-west-1.amazonaws.com"}) |
592 | + connect = self.mocker.replace("boto.ec2.connect_to_region") |
593 | + connect("us-west-1") |
594 | + |
595 | + def get_volumes_error(): |
596 | + raise EC2ResponseError(401, "Unauthorized") |
597 | + |
598 | + self.mocker.result( |
599 | + MockBotoEC2Connection(get_volumes=get_volumes_error)) |
600 | + self.mocker.replay() |
601 | + |
602 | + result = self.assertRaises( |
603 | + SystemExit, self.storage._ec2_validate_credentials) |
604 | + self.assertEqual(result.code, 1) |
605 | + message = ( |
606 | + "ERROR: Invalid EC2 credentials in /etc/boto.cfg. Unauthorized.") |
607 | + self.assertIn( |
608 | + message, util.hookenv._log_ERROR, "Not logged- %s" % message) |
609 | + |
610 | + def test_wb_ec2_validate_credentials_timeout(self): |
611 | + """ |
612 | + L{_ec2_validate_credentials} will exit in error when a connection |
613 | + timeout occurs against the region endpoint. |
614 | + """ |
615 | + ec2_url = "https://ec2.us-west-1.amazonaws.com" |
616 | + self._set_environment_vars({"EC2_URL": ec2_url}) |
617 | + connect = self.mocker.replace("boto.ec2.connect_to_region") |
618 | + connect("us-west-1") |
619 | + |
620 | + def get_volumes_error(): |
621 | + raise gaierror("Temporary failure in name resolution") |
622 | + |
623 | + self.mocker.result( |
624 | + MockBotoEC2Connection(get_volumes=get_volumes_error)) |
625 | + self.mocker.replay() |
626 | + |
627 | + result = self.assertRaises( |
628 | + SystemExit, self.storage._ec2_validate_credentials) |
629 | + self.assertEqual(result.code, 1) |
630 | + message = "ERROR: Connectivity error accessing %s" % ec2_url |
631 | self.assertIn( |
632 | message, util.hookenv._log_ERROR, "Not logged- %s" % message) |
633 | |
634 | def test_wb_ec2_describe_volumes_without_attached_instances(self): |
635 | """ |
636 | - L{_ec2_describe_volumes} parses the results of euca2ools |
637 | + L{_ec2_describe_volumes} parses the results of boto.ec2.get_all_volumes |
638 | C{DescribeVolumes} to create a C{dict} of volume information. When no |
639 | C{instance_id}s are present the volumes are not attached so no |
640 | C{device} or C{instance_id} information will be present. |
641 | @@ -1272,10 +1360,11 @@ |
642 | "456-456-456", device="/dev/notshown", instance_id="notseen", |
643 | zone="ec2-az2", size="8", status="available", |
644 | snapshot_id="some-shot", tags={"volume_name": "my volume name"}) |
645 | - euca_command = self.mocker.replace(self.storage.ec2_volume_class) |
646 | - euca_command() |
647 | - self.mocker.result(MockEucaCommand([volume1, volume2])) |
648 | - self.mocker.replay() |
649 | + |
650 | + def get_volumes(): |
651 | + return [volume1, volume2] |
652 | + |
653 | + self.storage.ec2_conn = MockBotoEC2Connection(get_volumes=get_volumes) |
654 | |
655 | expected = {"123-123-123": {"id": "123-123-123", "status": "available", |
656 | "device": "", |
657 | @@ -1295,9 +1384,8 @@ |
658 | |
659 | def test_wb_ec2_describe_volumes_matches_volume_id_supplied(self): |
660 | """ |
661 | - L{_ec2_describe_volumes} parses the results of euca2ools |
662 | - C{DescribeVolumes} to create a C{dict} of volume information. |
663 | - When C{volume_id} is provided return a C{dict} for the matched volume. |
664 | + L{_ec2_describe_volumes} matches the provided C{volume_id} to the |
665 | + results of boto's L{get_all_volumes}. |
666 | """ |
667 | volume_id = "123-123-123" |
668 | volume1 = MockVolume( |
669 | @@ -1308,10 +1396,11 @@ |
670 | "456-456-456", device="/dev/notshown", instance_id="notseen", |
671 | zone="ec2-az2", size="8", status="available", |
672 | snapshot_id="some-shot", tags={"volume_name": "my volume name"}) |
673 | - euca_command = self.mocker.replace(self.storage.ec2_volume_class) |
674 | - euca_command() |
675 | - self.mocker.result(MockEucaCommand([volume1, volume2])) |
676 | - self.mocker.replay() |
677 | + |
678 | + def get_volumes(): |
679 | + return [volume1, volume2] |
680 | + |
681 | + self.storage.ec2_conn = MockBotoEC2Connection(get_volumes=get_volumes) |
682 | |
683 | expected = { |
684 | "id": volume_id, "status": "available", "device": "", |
685 | @@ -1323,29 +1412,28 @@ |
686 | |
687 | def test_wb_ec2_describe_volumes_unmatched_volume_id_supplied(self): |
688 | """ |
689 | - L{_ec2_describe_volumes} parses the results of euca2ools |
690 | - C{DescribeVolumes} to create a C{dict} of volume information. |
691 | - When C{volume_id} is provided and unmatched, return an empty C{dict}. |
692 | + L{_ec2_describe_volumes} returns an empty C{dict} when it does not |
693 | + match the provided C{volume_id} to any volumes reported by boto's |
694 | + L{get_all_volumes} method. |
695 | """ |
696 | unmatched_volume_id = "456-456-456" |
697 | volume1 = MockVolume( |
698 | "123-123-123", device="/dev/notshown", instance_id="notseen", |
699 | zone="ec2-az1", size="10", status="available", |
700 | snapshot_id="some-shot", tags={}) |
701 | - euca_command = self.mocker.replace(self.storage.ec2_volume_class) |
702 | - euca_command() |
703 | - self.mocker.result(MockEucaCommand([volume1])) |
704 | - self.mocker.replay() |
705 | + |
706 | + def get_volumes(): |
707 | + return [volume1] |
708 | + |
709 | + self.storage.ec2_conn = MockBotoEC2Connection(get_volumes=get_volumes) |
710 | |
711 | self.assertEqual( |
712 | self.storage._ec2_describe_volumes(unmatched_volume_id), {}) |
713 | |
714 | def test_wb_ec2_describe_volumes_with_attached_instances(self): |
715 | """ |
716 | - L{_ec2_describe_volumes} parses the results of euca2ools |
717 | - C{DescribeVolumes} to create a C{dict} of volume information. If |
718 | - C{status} is C{in-use}, both C{device} and C{instance_id} will be |
719 | - returned in the C{dict}. |
720 | + L{_ec2_describe_volumes} will report attached instance_id information |
721 | + when boto's L{get_all_volumes} lists instance information for a volume. |
722 | """ |
723 | volume1 = MockVolume( |
724 | "123-123-123", device="/dev/notshown", instance_id="notseen", |
725 | @@ -1355,10 +1443,11 @@ |
726 | "456-456-456", device="/dev/xvdc", instance_id="i-456456", |
727 | zone="ec2-az2", size="8", status="in-use", |
728 | snapshot_id="some-shot", tags={"volume_name": "my volume name"}) |
729 | - euca_command = self.mocker.replace(self.storage.ec2_volume_class) |
730 | - euca_command() |
731 | - self.mocker.result(MockEucaCommand([volume1, volume2])) |
732 | - self.mocker.replay() |
733 | + |
734 | + def get_volumes(): |
735 | + return [volume1, volume2] |
736 | + |
737 | + self.storage.ec2_conn = MockBotoEC2Connection(get_volumes=get_volumes) |
738 | |
739 | expected = {"123-123-123": {"id": "123-123-123", "status": "available", |
740 | "device": "", |
741 | @@ -1379,41 +1468,38 @@ |
742 | |
743 | def test_wb_ec2_create_volume(self): |
744 | """ |
745 | - L{_ec2_create_volume} uses the command C{euca-create-volume} to create |
746 | + L{_ec2_create_volume} calls boto's L{create_volume} to create |
747 | a volume. It determines the availability zone for the volume by |
748 | - querying L{_ec2_describe_instances} on the provided C{instance_id} |
749 | - to ensure it matches the same availablity zone. It will then call |
750 | - L{_ec2_create_tag} to setup the C{volume_name} tag for the volume. |
751 | + querying L{_ec2_describe_instances} for the provided C{instance_id} |
752 | + to ensure the volume is created in the same availablity zone. |
753 | + L{_ec2_create_volume} will also call L{_ec2_create_tag} to setup the |
754 | + C{volume_name} tag for the volume. |
755 | """ |
756 | instance_id = "i-123123" |
757 | volume_id = "123-123-123" |
758 | volume_label = "postgresql/0 unit volume" |
759 | size = 10 |
760 | zone = "ec2-az3" |
761 | - command = "euca-create-volume -z %s -s %s" % (zone, size) |
762 | |
763 | reservation = MockEucaReservation( |
764 | [MockEucaInstance( |
765 | instance_id=instance_id, availability_zone=zone)]) |
766 | - euca_command = self.mocker.replace(self.storage.ec2_instance_class) |
767 | - euca_command() |
768 | - self.mocker.result(MockEucaCommand([reservation])) |
769 | - create = self.mocker.replace(subprocess.check_output) |
770 | - create(command, shell=True) |
771 | - self.mocker.result( |
772 | - "VOLUME %s 9 nova creating 2014-03-14T17:26:20\n" % volume_id) |
773 | - self.mocker.replay() |
774 | - |
775 | - def mock_describe_volumes(my_id): |
776 | - self.assertEqual(my_id, volume_id) |
777 | - return {"id": volume_id} |
778 | - self.storage._ec2_describe_volumes = mock_describe_volumes |
779 | - |
780 | - def mock_create_tag(my_id, key, value): |
781 | - self.assertEqual(my_id, volume_id) |
782 | - self.assertEqual(key, "volume_name") |
783 | - self.assertEqual(value, volume_label) |
784 | - self.storage._ec2_create_tag = mock_create_tag |
785 | + |
786 | + def get_instances(): |
787 | + return [reservation] |
788 | + |
789 | + def create_volume(size, zone): |
790 | + self.assertEqual(zone, "ec2-az3") |
791 | + self.assertEqual(size, 10) |
792 | + return MockVolume(vol_id=volume_id) |
793 | + |
794 | + def create_tags(resource_ids, tags): |
795 | + self.assertEqual(resource_ids, [volume_id]) |
796 | + self.assertEqual({"volume_name": volume_label}, tags) |
797 | + |
798 | + self.storage.ec2_conn = MockBotoEC2Connection( |
799 | + get_instances=get_instances, create_volume=create_volume, |
800 | + create_tags=create_tags) |
801 | |
802 | self.assertEqual( |
803 | self.storage._ec2_create_volume(size, volume_label, instance_id), |
804 | @@ -1436,10 +1522,11 @@ |
805 | volume_label = "postgresql/0 unit volume" |
806 | size = 10 |
807 | |
808 | - euca_command = self.mocker.replace(self.storage.ec2_instance_class) |
809 | - euca_command() |
810 | - self.mocker.result(MockEucaCommand([])) # Empty results from euca |
811 | - self.mocker.replay() |
812 | + def get_instances(): |
813 | + return [] |
814 | + |
815 | + self.storage.ec2_conn = MockBotoEC2Connection( |
816 | + get_instances=get_instances) |
817 | |
818 | result = self.assertRaises( |
819 | SystemExit, self.storage._ec2_create_volume, size, volume_label, |
820 | @@ -1448,174 +1535,85 @@ |
821 | config = util.hookenv.config_get() |
822 | message = ( |
823 | "ERROR: Could not create volume for instance %s. No instance " |
824 | - "details discovered by euca-describe-instances. Maybe the " |
825 | + "details discovered by boto.ec2.get_all_instances. Maybe the " |
826 | "charm configured endpoint %s is not valid for this region." % |
827 | (instance_id, config["endpoint"])) |
828 | self.assertIn( |
829 | message, util.hookenv._log_ERROR, "Not logged- %s" % message) |
830 | |
831 | - def test_wb_ec2_create_volume_error_invalid_response_type(self): |
832 | + def test_wb_ec2_create_volume_error(self): |
833 | """ |
834 | L{_ec2_create_volume} will log an error and exit when it receives an |
835 | - unparseable response type from the C{euca-create-volume} command. |
836 | - """ |
837 | - instance_id = "i-123123" |
838 | - volume_label = "postgresql/0 unit volume" |
839 | - size = 10 |
840 | - zone = "ec2-az3" |
841 | - command = "euca-create-volume -z %s -s %s" % (zone, size) |
842 | - |
843 | - reservation = MockEucaReservation( |
844 | - [MockEucaInstance( |
845 | - instance_id=instance_id, availability_zone=zone)]) |
846 | - euca_command = self.mocker.replace(self.storage.ec2_instance_class) |
847 | - euca_command() |
848 | - self.mocker.result(MockEucaCommand([reservation])) |
849 | - create = self.mocker.replace(subprocess.check_output) |
850 | - create(command, shell=True) |
851 | - self.mocker.result("INSTANCE invalid-instance-type-response\n") |
852 | - self.mocker.replay() |
853 | - |
854 | - def mock_describe_volumes(my_id): |
855 | - raise Exception("_ec2_describe_volumes should not be called") |
856 | - self.storage._ec2_describe_volumes = mock_describe_volumes |
857 | - |
858 | - def mock_create_tag(my_id, key, value): |
859 | - raise Exception("_ec2_create_tag should not be called") |
860 | - self.storage._ec2_create_tag = mock_create_tag |
861 | - |
862 | - result = self.assertRaises( |
863 | - SystemExit, self.storage._ec2_create_volume, size, volume_label, |
864 | - instance_id) |
865 | - self.assertEqual(result.code, 1) |
866 | - message = ( |
867 | - "ERROR: Didn't get VOLUME response from euca-create-volume. " |
868 | - "Response: INSTANCE invalid-instance-type-response\n") |
869 | - self.assertIn( |
870 | - message, util.hookenv._log_ERROR, "Not logged- %s" % message) |
871 | - |
872 | - def test_wb_ec2_create_volume_error_new_volume_not_found(self): |
873 | - """ |
874 | - L{_ec2_create_volume} will log an error and exit when it cannot find |
875 | - details of the newly created C{volume_id} through a subsequent call to |
876 | - L{_ec2_descibe_volumes}. |
877 | - """ |
878 | - instance_id = "i-123123" |
879 | - volume_id = "123-123-123" |
880 | - volume_label = "postgresql/0 unit volume" |
881 | - size = 10 |
882 | - zone = "ec2-az3" |
883 | - command = "euca-create-volume -z %s -s %s" % (zone, size) |
884 | - |
885 | - reservation = MockEucaReservation( |
886 | - [MockEucaInstance( |
887 | - instance_id=instance_id, availability_zone=zone)]) |
888 | - euca_command = self.mocker.replace(self.storage.ec2_instance_class) |
889 | - euca_command() |
890 | - self.mocker.result(MockEucaCommand([reservation])) |
891 | - create = self.mocker.replace(subprocess.check_output) |
892 | - create(command, shell=True) |
893 | - self.mocker.result( |
894 | - "VOLUME %s 9 nova creating 2014-03-14T17:26:20\n" % volume_id) |
895 | - self.mocker.replay() |
896 | - |
897 | - def mock_describe_volumes(my_id): |
898 | - self.assertEqual(my_id, volume_id) |
899 | - return {} # No details found for this volume |
900 | - self.storage._ec2_describe_volumes = mock_describe_volumes |
901 | - |
902 | - def mock_create_tag(my_id, key, value): |
903 | - raise Exception("_ec2_create_tag should not be called") |
904 | - self.storage._ec2_create_tag = mock_create_tag |
905 | - |
906 | - result = self.assertRaises( |
907 | - SystemExit, self.storage._ec2_create_volume, size, volume_label, |
908 | - instance_id) |
909 | - self.assertEqual(result.code, 1) |
910 | - message = ( |
911 | - "ERROR: Unable to find volume '%s'" % volume_id) |
912 | - self.assertIn( |
913 | - message, util.hookenv._log_ERROR, "Not logged- %s" % message) |
914 | - |
915 | - def test_wb_ec2_create_volume_error_command_failed(self): |
916 | - """ |
917 | - L{_ec2_create_volume} will log an error and exit when the |
918 | - C{euca-create-volume} fails. |
919 | - """ |
920 | - instance_id = "i-123123" |
921 | - volume_label = "postgresql/0 unit volume" |
922 | - size = 10 |
923 | - zone = "ec2-az3" |
924 | - command = "euca-create-volume -z %s -s %s" % (zone, size) |
925 | - |
926 | - reservation = MockEucaReservation( |
927 | - [MockEucaInstance( |
928 | - instance_id=instance_id, availability_zone=zone)]) |
929 | - euca_command = self.mocker.replace(self.storage.ec2_instance_class) |
930 | - euca_command() |
931 | - self.mocker.result(MockEucaCommand([reservation])) |
932 | - create = self.mocker.replace(subprocess.check_output) |
933 | - create(command, shell=True) |
934 | - self.mocker.throw(subprocess.CalledProcessError(1, command)) |
935 | - self.mocker.replay() |
936 | - |
937 | - def mock_exception(my_id): |
938 | - raise Exception("These methods should not be called") |
939 | - self.storage._ec2_describe_volumes = mock_exception |
940 | - self.storage._ec2_create_tag = mock_exception |
941 | - |
942 | - result = self.assertRaises( |
943 | - SystemExit, self.storage._ec2_create_volume, size, volume_label, |
944 | - instance_id) |
945 | - self.assertEqual(result.code, 1) |
946 | - |
947 | - message = ( |
948 | - "ERROR: Command '%s' returned non-zero exit status 1" % command) |
949 | + exception from boto's C{create_volume} command. |
950 | + """ |
951 | + instance_id = "i-123123" |
952 | + volume_label = "postgresql/0 unit volume" |
953 | + size = 10 |
954 | + zone = "ec2-az3" |
955 | + |
956 | + reservation = MockEucaReservation( |
957 | + [MockEucaInstance( |
958 | + instance_id=instance_id, availability_zone=zone)]) |
959 | + |
960 | + def get_instances(): |
961 | + return [reservation] |
962 | + |
963 | + def create_volume(size, zone): |
964 | + self.assertEqual(zone, "ec2-az3") |
965 | + self.assertEqual(size, 10) |
966 | + raise EC2ResponseError(401, "Unauthorized") |
967 | + |
968 | + self.storage.ec2_conn = MockBotoEC2Connection( |
969 | + get_instances=get_instances, create_volume=create_volume) |
970 | + |
971 | + result = self.assertRaises( |
972 | + SystemExit, self.storage._ec2_create_volume, size, volume_label, |
973 | + instance_id) |
974 | + self.assertEqual(result.code, 1) |
975 | + message = "ERROR: EC2ResponseError: 401 Unauthorized\n" |
976 | self.assertIn( |
977 | message, util.hookenv._log_ERROR, "Not logged- %s" % message) |
978 | |
979 | def test_wb_ec2_attach_volume(self): |
980 | """ |
981 | - L{_ec2_attach_volume} uses the command C{euca-attach-volume} and |
982 | - returns the attached volume path. |
983 | + L{_ec2_attach_volume} uses the EC2 boto's C{attach-volume} and |
984 | + returns the attached device path. |
985 | """ |
986 | instance_id = "i-123123" |
987 | volume_id = "123-123-123" |
988 | device = "/dev/xvdc" |
989 | - command = ( |
990 | - "euca-attach-volume -i %s -d %s %s" % |
991 | - (instance_id, device, volume_id)) |
992 | - |
993 | - attach = self.mocker.replace(subprocess.check_call) |
994 | - attach(command, shell=True) |
995 | - self.mocker.replay() |
996 | + |
997 | + def attach_volume(volume_id, instance_id, device): |
998 | + self.assertEqual(instance_id, "i-123123") |
999 | + self.assertEqual(volume_id, "123-123-123") |
1000 | + self.assertEqual(device, "/dev/xvdc") |
1001 | + |
1002 | + self.storage.ec2_conn = MockBotoEC2Connection( |
1003 | + attach_volume=attach_volume) |
1004 | |
1005 | self.assertEqual( |
1006 | self.storage._ec2_attach_volume(instance_id, volume_id), |
1007 | device) |
1008 | |
1009 | - def test_wb_ec2_attach_volume_command_failed(self): |
1010 | + def test_wb_ec2_attach_volume_failed(self): |
1011 | """ |
1012 | - L{_ec2_attach_volume} exits in error when C{euca-attach-volume} fails. |
1013 | + L{_ec2_attach_volume} exits in error when boto's C{attach-volume} |
1014 | + fails. |
1015 | """ |
1016 | instance_id = "i-123123" |
1017 | volume_id = "123-123-123" |
1018 | - device = "/dev/xvdc" |
1019 | - command = ( |
1020 | - "euca-attach-volume -i %s -d %s %s" % |
1021 | - (instance_id, device, volume_id)) |
1022 | - |
1023 | - attach = self.mocker.replace(subprocess.check_call) |
1024 | - attach(command, shell=True) |
1025 | - self.mocker.throw(subprocess.CalledProcessError(1, command)) |
1026 | - self.mocker.replay() |
1027 | + |
1028 | + def attach_volume(volume_id, instance_id, device): |
1029 | + raise EC2ResponseError(401, "Unauthorized") |
1030 | + |
1031 | + self.storage.ec2_conn = MockBotoEC2Connection( |
1032 | + attach_volume=attach_volume) |
1033 | |
1034 | result = self.assertRaises( |
1035 | SystemExit, self.storage._ec2_attach_volume, instance_id, |
1036 | volume_id) |
1037 | self.assertEqual(result.code, 1) |
1038 | - message = ( |
1039 | - "ERROR: Command '%s' returned non-zero exit status 1" % command) |
1040 | + message = "ERROR: EC2ResponseError: 401 Unauthorized\n" |
1041 | self.assertIn( |
1042 | message, util.hookenv._log_ERROR, "Not logged- %s" % message) |
1043 | |
1044 | @@ -1661,46 +1659,11 @@ |
1045 | self.assertIn( |
1046 | message, util.hookenv._log_INFO, "Not logged- %s" % message) |
1047 | |
1048 | - def test_detach_volume_command_error(self): |
1049 | - """ |
1050 | - When the C{euca-detach-volume} command fails, L{detach_volume} will |
1051 | - log a message and exit in error. |
1052 | - """ |
1053 | - volume_label = "postgresql/0 unit volume" |
1054 | - volume_id = "123-123-123" |
1055 | - instance_id = "i-123123" |
1056 | - self.storage.load_environment = lambda: None |
1057 | - |
1058 | - def mock_get_volume_id(label): |
1059 | - self.assertEqual(label, volume_label) |
1060 | - return volume_id |
1061 | - self.storage.get_volume_id = mock_get_volume_id |
1062 | - |
1063 | - def mock_describe_volumes(my_id): |
1064 | - self.assertEqual(my_id, volume_id) |
1065 | - return {"status": "in-use", "instance_id": instance_id} |
1066 | - self.storage.describe_volumes = mock_describe_volumes |
1067 | - |
1068 | - command = "euca-detach-volume -i %s %s" % (instance_id, volume_id) |
1069 | - ec2_cmd = self.mocker.replace(subprocess.check_call) |
1070 | - ec2_cmd(command, shell=True) |
1071 | - self.mocker.throw(subprocess.CalledProcessError(1, command)) |
1072 | - self.mocker.replay() |
1073 | - |
1074 | - result = self.assertRaises( |
1075 | - SystemExit, self.storage.detach_volume, volume_label) |
1076 | - self.assertEqual(result.code, 1) |
1077 | - message = ( |
1078 | - "ERROR: Couldn't detach volume. Command '%s' returned non-zero " |
1079 | - "exit status 1" % command) |
1080 | - self.assertIn( |
1081 | - message, util.hookenv._log_ERROR, "Not logged- %s" % message) |
1082 | - |
1083 | def test_detach_volume(self): |
1084 | """ |
1085 | When L{get_volume_id} finds a volume associated with this instance |
1086 | which has a volume state not equal to C{available}, it detaches that |
1087 | - volume using euca2ools commands. |
1088 | + volume using boto's L{detach_volume}. |
1089 | """ |
1090 | volume_label = "postgresql/0 unit volume" |
1091 | volume_id = "123-123-123" |
1092 | @@ -1717,10 +1680,12 @@ |
1093 | return {"status": "in-use", "instance_id": instance_id} |
1094 | self.storage.describe_volumes = mock_describe_volumes |
1095 | |
1096 | - command = "euca-detach-volume -i %s %s" % (instance_id, volume_id) |
1097 | - ec2_cmd = self.mocker.replace(subprocess.check_call) |
1098 | - ec2_cmd(command, shell=True) |
1099 | - self.mocker.replay() |
1100 | + def detach_volume(instance_id, volume_id): |
1101 | + self.assertEqual(instance_id, "i-123123") |
1102 | + self.assertEqual(volume_id, "123-123-123") |
1103 | + |
1104 | + self.storage.ec2_conn = MockBotoEC2Connection( |
1105 | + detach_volume=detach_volume) |
1106 | |
1107 | self.storage.detach_volume(volume_label) |
1108 | message = ( |
1109 | @@ -1728,3 +1693,38 @@ |
1110 | (volume_id, instance_id)) |
1111 | self.assertIn( |
1112 | message, util.hookenv._log_INFO, "Not logged- %s" % message) |
1113 | + |
1114 | + def test_detach_volume_error(self): |
1115 | + """ |
1116 | + An error is logged and we exit(1) when boto's L{detach_volume} raises |
1117 | + an error. |
1118 | + """ |
1119 | + volume_label = "postgresql/0 unit volume" |
1120 | + volume_id = "123-123-123" |
1121 | + instance_id = "i-123123" |
1122 | + self.storage.load_environment = lambda: None |
1123 | + |
1124 | + def mock_get_volume_id(label): |
1125 | + self.assertEqual(label, volume_label) |
1126 | + return volume_id |
1127 | + self.storage.get_volume_id = mock_get_volume_id |
1128 | + |
1129 | + def mock_describe_volumes(my_id): |
1130 | + self.assertEqual(my_id, volume_id) |
1131 | + return {"status": "in-use", "instance_id": instance_id} |
1132 | + self.storage.describe_volumes = mock_describe_volumes |
1133 | + |
1134 | + def detach_volume_error(instance_id, volume_id): |
1135 | + raise EC2ResponseError(401, "Unauthorized") |
1136 | + |
1137 | + self.storage.ec2_conn = MockBotoEC2Connection( |
1138 | + detach_volume=detach_volume_error) |
1139 | + |
1140 | + result = self.assertRaises( |
1141 | + SystemExit, self.storage.detach_volume, volume_label) |
1142 | + self.assertEqual(result.code, 1) |
1143 | + message = ( |
1144 | + "ERROR: Couldn't detach volume. EC2ResponseError: " |
1145 | + "401 Unauthorized\n") |
1146 | + self.assertIn( |
1147 | + message, util.hookenv._log_ERROR, "Not logged- %s" % message) |
1148 | |
1149 | === modified file 'hooks/util.py' |
1150 | --- hooks/util.py 2014-07-18 00:58:58 +0000 |
1151 | +++ hooks/util.py 2014-08-22 13:42:09 +0000 |
1152 | @@ -3,6 +3,7 @@ |
1153 | from charmhelpers.core import hookenv |
1154 | import subprocess |
1155 | import os |
1156 | +import re |
1157 | import sys |
1158 | from time import sleep |
1159 | |
1160 | @@ -17,11 +18,8 @@ |
1161 | "ec2": ["endpoint", "key", "secret"], |
1162 | "nova": ["endpoint", "region", "tenant", "key", "secret"]} |
1163 | |
1164 | -PROVIDER_COMMANDS = { |
1165 | - "ec2": {"validate": "euca-describe-instances", |
1166 | - "detach": "euca-detach-volume -i %s %s"}, |
1167 | - "nova": {"validate": "nova list", |
1168 | - "detach": "nova volume-detach %s %s"}} |
1169 | +# Use python-boto for AWS describe-volumes describe-instances |
1170 | +EC2_BOTO_CONFIG_FILE = "/etc/boto.cfg" |
1171 | |
1172 | |
1173 | class StorageServiceUtil(object): |
1174 | @@ -31,7 +29,6 @@ |
1175 | provider = None |
1176 | environment_map = None |
1177 | required_config_options = None |
1178 | - commands = None |
1179 | |
1180 | def __init__(self, provider): |
1181 | self.provider = provider |
1182 | @@ -43,13 +40,8 @@ |
1183 | hookenv.ERROR) |
1184 | sys.exit(1) |
1185 | self.environment_map = ENVIRONMENT_MAP[provider] |
1186 | - self.commands = PROVIDER_COMMANDS[provider] |
1187 | self.required_config_options = REQUIRED_CONFIG_OPTIONS[provider] |
1188 | - if provider == "ec2": |
1189 | - import euca2ools.commands.euca.describevolumes as getvolumes |
1190 | - import euca2ools.commands.euca.describeinstances as getinstances |
1191 | - self.ec2_volume_class = getvolumes.DescribeVolumes |
1192 | - self.ec2_instance_class = getinstances.DescribeInstances |
1193 | + self.ec2_conn = None |
1194 | |
1195 | def load_environment(self): |
1196 | """ |
1197 | @@ -60,18 +52,26 @@ |
1198 | for option in self.required_config_options: |
1199 | environment_variable = self.environment_map[option] |
1200 | os.environ[environment_variable] = config_data[option].strip() |
1201 | + if self.provider == "ec2": |
1202 | + self._setup_boto_config( |
1203 | + os.environ.get("EC2_ACCESS_KEY", None), |
1204 | + os.environ.get("EC2_SECRET_KEY", None)) |
1205 | self.validate_credentials() |
1206 | |
1207 | def validate_credentials(self): |
1208 | + method = getattr(self, "_%s_validate_credentials" % self.provider) |
1209 | + return method() |
1210 | + |
1211 | + def _nova_validate_credentials(self): |
1212 | """ |
1213 | - Attempt to contact the respective ec2 or nova volume service or exit(1) |
1214 | + Attempt to contact nova volume service or exit(1) |
1215 | """ |
1216 | try: |
1217 | - subprocess.check_call(self.commands["validate"], shell=True) |
1218 | + subprocess.check_call("nova list", shell=True) |
1219 | except subprocess.CalledProcessError, e: |
1220 | hookenv.log( |
1221 | - "ERROR: Charm configured credentials can't access endpoint. " |
1222 | - "%s" % str(e), |
1223 | + "ERROR: Charm configured credentials can't access nova " |
1224 | + "endpoint. %s" % str(e), |
1225 | hookenv.ERROR) |
1226 | sys.exit(1) |
1227 | hookenv.log( |
1228 | @@ -177,8 +177,8 @@ |
1229 | sleep(5) |
1230 | if not device: |
1231 | hookenv.log( |
1232 | - "ERROR: Unable to discover device attached by " |
1233 | - "euca-attach-volume", |
1234 | + "ERROR: Unable to discover device attached using " |
1235 | + "python boto.ec2.attach_volume.", |
1236 | hookenv.ERROR) |
1237 | sys.exit(1) |
1238 | return device |
1239 | @@ -201,9 +201,15 @@ |
1240 | hookenv.log( |
1241 | "Detaching volume (%s) from instance %s" % |
1242 | (volume_id, volume["instance_id"])) |
1243 | + |
1244 | + method = getattr(self, "_%s_detach_volume" % self.provider) |
1245 | + return method(volume_id=volume_id, instance_id=volume["instance_id"]) |
1246 | + |
1247 | + def _nova_detach_volume(self, volume_id, instance_id): |
1248 | + """Detach specified C{volume_id} from the nova C{instance_id}.""" |
1249 | try: |
1250 | subprocess.check_call( |
1251 | - self.commands["detach"] % (volume["instance_id"], volume_id), |
1252 | + "nova volume-detach %s %s" % (instance_id, volume_id), |
1253 | shell=True) |
1254 | except subprocess.CalledProcessError, e: |
1255 | hookenv.log( |
1256 | @@ -212,35 +218,72 @@ |
1257 | return |
1258 | |
1259 | # EC2-specific methods |
1260 | - def _ec2_create_tag(self, volume_id, tag_name, tag_value=None): |
1261 | + def _setup_boto_config(self, key, secret): |
1262 | + """Write EC2 credentials to C{EC2_BOTO_CONFIG_FILE}.""" |
1263 | + with open(EC2_BOTO_CONFIG_FILE, "w") as config_file: |
1264 | + config_file.write( |
1265 | + "[Credentials]\naws_access_key_id = %s\n" |
1266 | + "aws_secret_access_key = %s\n" % (key, secret)) |
1267 | + |
1268 | + def _ec2_validate_credentials(self): |
1269 | + """Attempt to contact EC2 storage service or exit(1).""" |
1270 | + from boto.exception import NoAuthHandlerFound, EC2ResponseError |
1271 | + import boto.ec2 |
1272 | + from socket import gaierror |
1273 | + message = None |
1274 | + if self.provider == "ec2" and not self.ec2_conn: |
1275 | + # parse region out of EC2_URL environment variable |
1276 | + ec2_url = os.environ.get("EC2_URL", "NOT SET") |
1277 | + matches = re.search("\S{2}-\S+-\d", ec2_url) |
1278 | + if not matches: |
1279 | + message = ( |
1280 | + "ERROR: Couldn't get region from EC2_URL environment " |
1281 | + "variable: %s." % ec2_url) |
1282 | + hookenv.log(message, hookenv.ERROR) |
1283 | + sys.exit(1) |
1284 | + ec2_region = matches.group(0) |
1285 | + |
1286 | + # Create connection object, doesn't actually contact AWS |
1287 | + self.ec2_conn = boto.ec2.connect_to_region(ec2_region) |
1288 | + try: |
1289 | + # Test credentials against AWS with a simple command |
1290 | + self.ec2_conn.get_all_volumes() |
1291 | + except EC2ResponseError: |
1292 | + message = ( |
1293 | + "ERROR: Invalid EC2 credentials in /etc/boto.cfg. " |
1294 | + "Unauthorized.") |
1295 | + except NoAuthHandlerFound: |
1296 | + message = ( |
1297 | + "ERROR: EC2 credentials not found in /etc/boto.cfg. " |
1298 | + "Cannot authenticate.") |
1299 | + except gaierror: |
1300 | + message = "ERROR: Connectivity error accessing %s" % ec2_url |
1301 | + if message: |
1302 | + hookenv.log(message, hookenv.ERROR) |
1303 | + sys.exit(1) |
1304 | + hookenv.log( |
1305 | + "Validated charm configuration credentials have access to " |
1306 | + "block storage service") |
1307 | + |
1308 | + def _ec2_create_tag(self, volume_id, tag_name, tag_value=""): |
1309 | """Attach a tag and optional C{tag_value} to the given C{volume_id}""" |
1310 | tag_string = tag_name |
1311 | if tag_value: |
1312 | tag_string += "=%s" % tag_value |
1313 | - command = 'euca-create-tags %s --tag "%s"' % (volume_id, tag_string) |
1314 | - |
1315 | try: |
1316 | - subprocess.check_call(command, shell=True) |
1317 | - except subprocess.CalledProcessError, e: |
1318 | - hookenv.log( |
1319 | - "ERROR: Couldn't add tags to the resource. %s" % str(e), |
1320 | - hookenv.ERROR) |
1321 | + self.ec2_conn.create_tags( |
1322 | + resource_ids=[volume_id], tags={tag_name: tag_value}) |
1323 | + except Exception, e: |
1324 | + hookenv.log("ERROR: %s" % str(e), hookenv.ERROR) |
1325 | sys.exit(1) |
1326 | hookenv.log("Tagged (%s) to %s." % (tag_string, volume_id)) |
1327 | |
1328 | def _ec2_describe_instances(self, instance_id=None): |
1329 | """ |
1330 | - Use euca2ools libraries to describe instances and return a C{dict} |
1331 | + Use AWS Dev API (boto) to describe instances and return a C{dict} |
1332 | """ |
1333 | result = {} |
1334 | - try: |
1335 | - command = self.ec2_instance_class() |
1336 | - reservations = command.main() |
1337 | - except SystemExit: |
1338 | - hookenv.log( |
1339 | - "ERROR: Couldn't contact EC2 using euca-describe-instances", |
1340 | - hookenv.ERROR) |
1341 | - sys.exit(1) |
1342 | + reservations = self.ec2_conn.get_all_instances() |
1343 | for reservation in reservations: |
1344 | for inst in reservation.instances: |
1345 | result[inst.id] = { |
1346 | @@ -259,17 +302,10 @@ |
1347 | |
1348 | def _ec2_describe_volumes(self, volume_id=None): |
1349 | """ |
1350 | - Use euca2ools libraries to describe volumes and return a C{dict} |
1351 | + Use AWS Developer API (boto) to describe volumes and return a C{dict} |
1352 | """ |
1353 | result = {} |
1354 | - try: |
1355 | - command = self.ec2_volume_class() |
1356 | - volumes = command.main() |
1357 | - except SystemExit: |
1358 | - hookenv.log( |
1359 | - "ERROR: Couldn't contact EC2 using euca-describe-volumes", |
1360 | - hookenv.ERROR) |
1361 | - sys.exit(1) |
1362 | + volumes = self.ec2_conn.get_all_volumes() |
1363 | for volume in volumes: |
1364 | result[volume.id] = { |
1365 | "device": "", |
1366 | @@ -306,34 +342,19 @@ |
1367 | config_data = hookenv.config() |
1368 | hookenv.log( |
1369 | "ERROR: Could not create volume for instance %s. No instance " |
1370 | - "details discovered by euca-describe-instances. Maybe the " |
1371 | + "details discovered by boto.ec2.get_all_instances. Maybe the " |
1372 | "charm configured endpoint %s is not valid for this region." % |
1373 | (instance_id, config_data["endpoint"]), hookenv.ERROR) |
1374 | sys.exit(1) |
1375 | |
1376 | try: |
1377 | - output = subprocess.check_output( |
1378 | - "euca-create-volume -z %s -s %s" % |
1379 | - (instance["availability_zone"], size), shell=True) |
1380 | - except subprocess.CalledProcessError, e: |
1381 | + volume = self.ec2_conn.create_volume( |
1382 | + size=size, zone=instance["availability_zone"]) |
1383 | + except Exception, e: |
1384 | hookenv.log("ERROR: %s" % str(e), hookenv.ERROR) |
1385 | sys.exit(1) |
1386 | - |
1387 | - response_type, volume_id = output.split()[:2] |
1388 | - if response_type != "VOLUME": |
1389 | - hookenv.log( |
1390 | - "ERROR: Didn't get VOLUME response from euca-create-volume. " |
1391 | - "Response: %s" % output, hookenv.ERROR) |
1392 | - sys.exit(1) |
1393 | - volume = self.describe_volumes(volume_id.strip()) |
1394 | - if not volume: |
1395 | - hookenv.log( |
1396 | - "ERROR: Unable to find volume '%s'" % volume_id.strip(), |
1397 | - hookenv.ERROR) |
1398 | - sys.exit(1) |
1399 | - volume_id = volume["id"] |
1400 | - self._ec2_create_tag(volume_id, "volume_name", volume_label) |
1401 | - return volume_id |
1402 | + self._ec2_create_tag(volume.id, "volume_name", volume_label) |
1403 | + return volume.id |
1404 | |
1405 | def _ec2_attach_volume(self, instance_id, volume_id): |
1406 | """ |
1407 | @@ -342,14 +363,25 @@ |
1408 | """ |
1409 | device = "/dev/xvdc" |
1410 | try: |
1411 | - subprocess.check_call( |
1412 | - "euca-attach-volume -i %s -d %s %s" % |
1413 | - (instance_id, device, volume_id), shell=True) |
1414 | - except subprocess.CalledProcessError, e: |
1415 | + self.ec2_conn.attach_volume( |
1416 | + volume_id=volume_id, instance_id=instance_id, device=device) |
1417 | + except Exception, e: |
1418 | hookenv.log("ERROR: %s" % str(e), hookenv.ERROR) |
1419 | sys.exit(1) |
1420 | return device |
1421 | |
1422 | + def _ec2_detach_volume(self, instance_id, volume_id): |
1423 | + """ |
1424 | + Detach an EC2 C{volume_id} from the provided C{instance_id}. |
1425 | + """ |
1426 | + try: |
1427 | + self.ec2_conn.detach_volume( |
1428 | + volume_id=volume_id, instance_id=instance_id) |
1429 | + except Exception, e: |
1430 | + hookenv.log( |
1431 | + "ERROR: Couldn't detach volume. %s" % str(e), hookenv.ERROR) |
1432 | + sys.exit(1) |
1433 | + |
1434 | # Nova-specific methods |
1435 | def _nova_volume_show(self, volume_id): |
1436 | """ |
Hey Chad, overall it looks great.
Just a few points inline.