Merge lp:~chad.smith/charms/precise/block-storage-broker/bsb-ec2-boto into lp:charms/block-storage-broker

Proposed by Chad Smith on 2014-08-19
Status: Merged
Approved by: David Britton on 2014-09-09
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
Reviewer Review Type Date Requested Status
David Britton (community) 2014-08-19 Approve on 2014-09-09
Fernando Correa Neto (community) 2014-08-19 Approve on 2014-08-22
Review via email: mp+231275@code.launchpad.net

Description of the change

This branch moves block-storage-broker's EC2-specific methods from euca2ools to AWS' python SDK (python-boto).

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-storage-bundle.cfg:
  - change all mention of precise to trusty
  - change postgresql-9.1-debversion to postgresql-9.3.debversion

Test procedure is something like the following:

1. Create postgresql-storage-bundle.cfg as exemplified below, replacing the EC2_* values with valid credentials and endpoints.

2. juju-bootstrap -e your-ec2-environment
# to deploy block-storage-broker, storage subordinate and postgresql and create and attach a volume.
3. juju-deployer -c postgresql-storage-bundle.cfg doit-no-volume

----- postgresql-storage-bundle.cfg -----
common:
    services:
        postgresql:
            branch: lp:~charmers/charms/precise/postgresql/trunk
            constraints: mem=2048
            options:
                extra-packages: python-apt postgresql-contrib postgresql-9.1-debversion
                max_connections: 500
        block-storage-broker:
            branch: lp:~chad.smith/charms/precise/block-storage-broker/bsb-ec2-boto
            options:
                provider: ec2
                key: <your EC2_ACCESS_KEY>
                endpoint: <your EC2_URL>
                secret: <your EC2_SECRET_KEY>

doit-no-volume:
    inherits: common
    series: precise
    services:
        storage:
            branch: lp:~charmers/charms/precise/storage/trunk
            options:
                provider: block-storage-broker
                volume_size: 9
    relations:
        - [postgresql, storage]
        - [storage, block-storage-broker]

doit-with-volume-map:
    inherits: common
    series: precise
    services:
        storage:
            branch: lp:~charmers/charms/precise/storage/trunk
            options:
                provider: block-storage-broker
                volume_size: 9
                volume_map: "{postgresql/0: YOUR-EXISTING-EUCA-VOLUME-ID}"

    relations:
        - [postgresql, storage]
        - [storage, block-storage-broker]

To post a comment you must log in.
60. By Chad Smith on 2014-08-19

mocker assert cloud-archive:havana is not added on trusty or later

61. By Chad Smith on 2014-08-19

fcorrea review comment. docstring update

Fernando Correa Neto (fcorrea) wrote :

Hey Chad, overall it looks great.

Just a few points inline.

review: Needs Fixing
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 on 2014-08-22

add apt_install and add_source params to install hook for testing

63. By Chad Smith on 2014-08-22

pull ec2_url regex parsing and connect_to_region calls out of try/except block to simplify error handling

64. By Chad Smith on 2014-08-22

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 on 2014-08-22

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.

review: Approve
66. By Chad Smith on 2014-08-22

consolidation of environment setup in a class method _set_environment_vars

David Britton (davidpbritton) 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.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 """

Subscribers

People subscribed via source and target branches

to all changes: