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

Proposed by Chad Smith
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
Reviewer Review Type Date Requested Status
David Britton (community) Approve
Fernando Correa Neto (community) Approve
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

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

61. By Chad Smith

fcorrea review comment. docstring update

Revision history for this message
Fernando Correa Neto (fcorrea) wrote :

Hey Chad, overall it looks great.

Just a few points inline.

review: Needs Fixing
Revision history for this message
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.

Revision history for this message
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

Revision history for this message
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

Revision history for this message
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

consolidation of environment setup in a class method _set_environment_vars

Revision history for this message
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.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'hooks/hooks.py'
--- hooks/hooks.py 2014-07-18 00:58:58 +0000
+++ hooks/hooks.py 2014-08-22 13:42:09 +0000
@@ -1,6 +1,7 @@
1#!/usr/bin/env python1#!/usr/bin/env python
2# vim: et ai ts=4 sw=4:2# vim: et ai ts=4 sw=4:
33
4from charmhelpers import fetch
4from charmhelpers.core import hookenv5from charmhelpers.core import hookenv
5from charmhelpers.core.hookenv import ERROR, INFO6from charmhelpers.core.hookenv import ERROR, INFO
67
@@ -77,18 +78,23 @@
7778
7879
79@hooks.hook()80@hooks.hook()
80def install():81def install(apt_install=None, add_source=None):
81 """Install required packages if not present"""82 """Install required packages if not present."""
82 from charmhelpers import fetch83
84 if apt_install is None: # for testing purposes
85 apt_install = fetch.apt_install
86 if add_source is None: # for testing purposes
87 add_source = fetch.add_source
88
83 provider = hookenv.config("provider")89 provider = hookenv.config("provider")
84 if provider == "nova":90 if provider == "nova":
85 required_packages = ["python-novaclient"]91 required_packages = ["python-novaclient"]
86 if int(get_running_series()['release'].split(".")[0]) < 14:92 if int(get_running_series()['release'].split(".")[0]) < 14:
87 fetch.add_source("cloud-archive:havana")93 add_source("cloud-archive:havana")
88 elif provider == "ec2":94 elif provider == "ec2":
89 required_packages = ["euca2ools"]95 required_packages = ["python-boto"]
90 fetch.apt_update(fatal=True)96 fetch.apt_update(fatal=True)
91 fetch.apt_install(required_packages, fatal=True)97 apt_install(required_packages, fatal=True)
9298
9399
94@hooks.hook("block-storage-relation-departed")100@hooks.hook("block-storage-relation-departed")
95101
=== modified file 'hooks/test_hooks.py'
--- hooks/test_hooks.py 2014-07-18 04:13:23 +0000
+++ hooks/test_hooks.py 2014-08-22 13:42:09 +0000
@@ -182,24 +182,27 @@
182 self.mocker.replay()182 self.mocker.replay()
183 hooks.config_changed()183 hooks.config_changed()
184184
185 def test_install_installs_novaclient(self):185 def test_install_installs_novaclient_without_cloud_archive(self):
186 """186 """
187 L{install} will call C{fetch.add_source} to add a cloud repository and187 On releases C{trusty} and later L{install} will install the
188 install the C{python-novaclient} package.188 python-novaclient package without installing the cloud-archive
189 repository.
189 """190 """
190 get_running_series = self.mocker.replace(hooks.get_running_series)191 get_running_series = self.mocker.replace(hooks.get_running_series)
191 get_running_series()192 get_running_series()
192 self.mocker.result({'release': '19.04'}) # Not precise193 self.mocker.result({'release': '14.04'}) # Trusty series
193 add_source = self.mocker.replace(fetch.add_source)194 add_source = self.mocker.replace(fetch.add_source)
194 # We are testing that add_source is not called.195 add_source("cloud-archive:havana")
195 # add_source("cloud-archive:havana")196 self.mocker.count(0) # Test we never called add_source
196 apt_update = self.mocker.replace(fetch.apt_update)197 apt_update = self.mocker.replace(fetch.apt_update)
197 apt_update(fatal=True)198 apt_update(fatal=True)
198 apt_install = self.mocker.replace(fetch.apt_install)
199 apt_install(["python-novaclient"], fatal=True)
200 self.mocker.replay()199 self.mocker.replay()
201200
202 hooks.install()201 def apt_install(packages, fatal):
202 self.assertEqual(["python-novaclient"], packages)
203 self.assertTrue(fatal)
204
205 hooks.install(apt_install=apt_install, add_source=add_source)
203206
204 def test_precise_install_adds_apt_source_and_installs_novaclient(self):207 def test_precise_install_adds_apt_source_and_installs_novaclient(self):
205 """208 """
@@ -209,15 +212,40 @@
209 get_running_series = self.mocker.replace(hooks.get_running_series)212 get_running_series = self.mocker.replace(hooks.get_running_series)
210 get_running_series()213 get_running_series()
211 self.mocker.result({'release': '12.04'}) # precise214 self.mocker.result({'release': '12.04'}) # precise
212 add_source = self.mocker.replace(fetch.add_source)215 apt_update = self.mocker.replace(fetch.apt_update)
213 add_source("cloud-archive:havana") # precise needs havana216 apt_update(fatal=True)
214 apt_update = self.mocker.replace(fetch.apt_update)217 self.mocker.replay()
215 apt_update(fatal=True)218
216 apt_install = self.mocker.replace(fetch.apt_install)219 def add_source(source):
217 apt_install(["python-novaclient"], fatal=True)220 self.assertEqual("cloud-archive:havana", source)
218 self.mocker.replay()221
219222 def apt_install(packages, fatal):
220 hooks.install()223 self.assertEqual(["python-novaclient"], packages)
224 self.assertTrue(fatal)
225
226 hooks.install(apt_install=apt_install, add_source=add_source)
227
228 def test_ec2_provider_install_installs_ec2_dependencies(self):
229 """
230 When the provider is configured as C{ec2}, L{install} will install
231 the C{python-boto} package.
232 """
233 self.addCleanup(
234 setattr, hooks.hookenv, "_config", hooks.hookenv._config)
235 hooks.hookenv._config = (
236 ("key", ""), ("tenant", ""), ("provider", "ec2"),
237 ("secret", ""), ("region", ""),
238 ("endpoint", ""))
239
240 apt_update = self.mocker.replace(fetch.apt_update)
241 apt_update(fatal=True)
242 self.mocker.replay()
243
244 def apt_install(packages, fatal):
245 self.assertEqual(["python-boto"], packages)
246 self.assertTrue(fatal)
247
248 hooks.install(apt_install=apt_install)
221249
222 def test_block_storage_relation_changed_waits_without_instance_id(self):250 def test_block_storage_relation_changed_waits_without_instance_id(self):
223 """251 """
224252
=== modified file 'hooks/test_util.py'
--- hooks/test_util.py 2014-03-21 17:42:00 +0000
+++ hooks/test_util.py 2014-08-22 13:42:09 +0000
@@ -1,7 +1,8 @@
1import util1import util
2from util import StorageServiceUtil, ENVIRONMENT_MAP, generate_volume_label2from util import StorageServiceUtil, ENVIRONMENT_MAP, generate_volume_label
3import mocker3import mocker
4import os4from boto.exception import NoAuthHandlerFound, EC2ResponseError
5from socket import gaierror
5import subprocess6import subprocess
6from testing import TestHookenv7from testing import TestHookenv
78
@@ -19,6 +20,10 @@
19 util.log = util.hookenv.log20 util.log = util.hookenv.log
20 self.storage = StorageServiceUtil("nova")21 self.storage = StorageServiceUtil("nova")
2122
23 def _set_environment_vars(self, environ={}):
24 self.addCleanup(setattr, util.os, "environ", util.os.environ)
25 util.os.environ = environ
26
22 def test_invalid_provier_config(self):27 def test_invalid_provier_config(self):
23 """When an invalid provider config is set and error is reported."""28 """When an invalid provider config is set and error is reported."""
24 result = self.assertRaises(SystemExit, StorageServiceUtil, "ce2")29 result = self.assertRaises(SystemExit, StorageServiceUtil, "ce2")
@@ -42,8 +47,7 @@
42 variables and then call L{validate_credentials} to assert47 variables and then call L{validate_credentials} to assert
43 that environment variables provided give access to the service.48 that environment variables provided give access to the service.
44 """49 """
45 self.addCleanup(setattr, util.os, "environ", util.os.environ)50 self._set_environment_vars({})
46 util.os.environ = {}
4751
48 def mock_validate():52 def mock_validate():
49 pass53 pass
@@ -64,7 +68,7 @@
64 L{load_environment} will exit in failure and log a message if any68 L{load_environment} will exit in failure and log a message if any
65 required configuration option is not set.69 required configuration option is not set.
66 """70 """
67 self.addCleanup(setattr, util.os, "environ", util.os.environ)71 self._set_environment_vars({})
6872
69 def mock_validate():73 def mock_validate():
70 raise SystemExit("something invalid")74 raise SystemExit("something invalid")
@@ -89,7 +93,7 @@
89 SystemExit, self.storage.validate_credentials)93 SystemExit, self.storage.validate_credentials)
90 self.assertEqual(result.code, 1)94 self.assertEqual(result.code, 1)
91 message = (95 message = (
92 "ERROR: Charm configured credentials can't access endpoint. "96 "ERROR: Charm configured credentials can't access nova endpoint. "
93 "Command '%s' returned non-zero exit status 1" % command)97 "Command '%s' returned non-zero exit status 1" % command)
94 self.assertIn(98 self.assertIn(
95 message, util.hookenv._log_ERROR, "Not logged- %s" % message)99 message, util.hookenv._log_ERROR, "Not logged- %s" % message)
@@ -239,9 +243,7 @@
239 with the os.environ[JUJU_REMOTE_UNIT].243 with the os.environ[JUJU_REMOTE_UNIT].
240 """244 """
241 unit_name = "postgresql/0"245 unit_name = "postgresql/0"
242 self.addCleanup(246 self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name})
243 setattr, os, "environ", os.environ)
244 os.environ = {"JUJU_REMOTE_UNIT": unit_name}
245 volume_id = "123134124-1241412-1242141"247 volume_id = "123134124-1241412-1242141"
246248
247 def mock_describe():249 def mock_describe():
@@ -259,9 +261,7 @@
259 for the os.environ[JUJU_REMOTE_UNIT].261 for the os.environ[JUJU_REMOTE_UNIT].
260 """262 """
261 unit_name = "postgresql/0"263 unit_name = "postgresql/0"
262 self.addCleanup(264 self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name})
263 setattr, os, "environ", os.environ)
264 os.environ = {"JUJU_REMOTE_UNIT": unit_name}
265265
266 def mock_describe(val):266 def mock_describe(val):
267 self.assertIsNone(val)267 self.assertIsNone(val)
@@ -280,8 +280,7 @@
280 multiple results the function exits with an error.280 multiple results the function exits with an error.
281 """281 """
282 unit_name = "postgresql/0"282 unit_name = "postgresql/0"
283 self.addCleanup(setattr, os, "environ", os.environ)283 self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name})
284 os.environ = {"JUJU_REMOTE_UNIT": unit_name}
285284
286 def mock_describe():285 def mock_describe():
287 return {"123123-123123":286 return {"123123-123123":
@@ -306,8 +305,7 @@
306 unit_name = "postgresql/0"305 unit_name = "postgresql/0"
307 instance_id = "i-123123"306 instance_id = "i-123123"
308 volume_id = "123-123-123"307 volume_id = "123-123-123"
309 self.addCleanup(setattr, os, "environ", os.environ)308 self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name})
310 os.environ = {"JUJU_REMOTE_UNIT": unit_name}
311309
312 self.storage.load_environment = lambda: None310 self.storage.load_environment = lambda: None
313 self.storage.describe_volumes = lambda volume_id: {}311 self.storage.describe_volumes = lambda volume_id: {}
@@ -331,8 +329,7 @@
331 volume_id = "123-123-123"329 volume_id = "123-123-123"
332 instance_id = "i-123123123"330 instance_id = "i-123123123"
333 volume_label = "%s unit volume" % unit_name331 volume_label = "%s unit volume" % unit_name
334 self.addCleanup(setattr, os, "environ", os.environ)332 self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name})
335 os.environ = {"JUJU_REMOTE_UNIT": unit_name}
336 self.storage.load_environment = lambda: None333 self.storage.load_environment = lambda: None
337334
338 def mock_get_volume_id(label):335 def mock_get_volume_id(label):
@@ -360,8 +357,7 @@
360 unit_name = "postgresql/0"357 unit_name = "postgresql/0"
361 instance_id = "i-123123"358 instance_id = "i-123123"
362 volume_id = "123-123-123"359 volume_id = "123-123-123"
363 self.addCleanup(setattr, os, "environ", os.environ)360 self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name})
364 os.environ = {"JUJU_REMOTE_UNIT": unit_name}
365361
366 self.storage.load_environment = lambda: None362 self.storage.load_environment = lambda: None
367363
@@ -385,14 +381,13 @@
385 unit_name = "postgresql/0"381 unit_name = "postgresql/0"
386 instance_id = "i-123123"382 instance_id = "i-123123"
387 volume_id = "123-123-123"383 volume_id = "123-123-123"
388 self.addCleanup(setattr, os, "environ", os.environ)384 self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name})
389 os.environ = {"JUJU_REMOTE_UNIT": unit_name}
390385
391 self.describe_count = 0386 self.describe_count = 0
392387
393 self.storage.load_environment = lambda: None388 self.storage.load_environment = lambda: None
394389
395 sleep = self.mocker.replace("util.sleep")390 sleep = self.mocker.replace("time.sleep")
396 sleep(5)391 sleep(5)
397 self.mocker.replay()392 self.mocker.replay()
398393
@@ -423,8 +418,7 @@
423 unit_name = "postgresql/0"418 unit_name = "postgresql/0"
424 instance_id = "i-123123"419 instance_id = "i-123123"
425 volume_id = "123-123-123"420 volume_id = "123-123-123"
426 self.addCleanup(setattr, os, "environ", os.environ)421 self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name})
427 os.environ = {"JUJU_REMOTE_UNIT": unit_name}
428422
429 self.storage.load_environment = lambda: None423 self.storage.load_environment = lambda: None
430424
@@ -452,8 +446,7 @@
452 volume_id = "123-123-123"446 volume_id = "123-123-123"
453 volume_label = "%s unit volume" % unit_name447 volume_label = "%s unit volume" % unit_name
454 default_volume_size = util.hookenv.config("default_volume_size")448 default_volume_size = util.hookenv.config("default_volume_size")
455 self.addCleanup(setattr, os, "environ", os.environ)449 self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name})
456 os.environ = {"JUJU_REMOTE_UNIT": unit_name}
457450
458 self.storage.load_environment = lambda: None451 self.storage.load_environment = lambda: None
459 self.storage.get_volume_id = lambda _: None452 self.storage.get_volume_id = lambda _: None
@@ -880,6 +873,19 @@
880 message, util.hookenv._log_INFO, "Not logged- %s" % message)873 message, util.hookenv._log_INFO, "Not logged- %s" % message)
881874
882875
876class MockBotoEC2Connection(object):
877 """Mock all calls to python-boto's EC2Connection class."""
878 def __init__(self, get_volumes=None, get_instances=None,
879 create_volume=None, create_tags=None, attach_volume=None,
880 detach_volume=None):
881 self.get_all_volumes = get_volumes
882 self.get_all_instances = get_instances
883 self.create_volume = create_volume
884 self.attach_volume = attach_volume
885 self.detach_volume = detach_volume
886 self.create_tags = create_tags
887
888
883class MockEucaCommand(object):889class MockEucaCommand(object):
884 def __init__(self, result):890 def __init__(self, result):
885 self.result = result891 self.result = result
@@ -918,8 +924,8 @@
918924
919925
920class MockVolume(object):926class MockVolume(object):
921 def __init__(self, vol_id, device, instance_id, zone, size, status,927 def __init__(self, vol_id, device=None, instance_id=None, zone=None,
922 snapshot_id, tags):928 size=None, status=None, snapshot_id=None, tags=None):
923 self.id = vol_id929 self.id = vol_id
924 self.attach_data = MockAttachData(device, instance_id)930 self.attach_data = MockAttachData(device, instance_id)
925 self.zone = zone931 self.zone = zone
@@ -936,11 +942,31 @@
936 self.maxDiff = None942 self.maxDiff = None
937 util.hookenv = TestHookenv(943 util.hookenv = TestHookenv(
938 {"key": "ec2key", "secret": "ec2password",944 {"key": "ec2key", "secret": "ec2password",
939 "endpoint": "https://ec2-region-url:443/v2.0/",945 "endpoint": "https://ec2-region-1.com",
940 "default_volume_size": 11})946 "default_volume_size": 11})
947 util.EC2_BOTO_CONFIG_FILE = self.makeFile()
941 util.log = util.hookenv.log948 util.log = util.hookenv.log
942 self.storage = StorageServiceUtil("ec2")949 self.storage = StorageServiceUtil("ec2")
943950
951 def _set_environment_vars(self, environ={}):
952 self.addCleanup(setattr, util.os, "environ", util.os.environ)
953 util.os.environ = environ
954
955 def test_wb_setup_boto_config(self):
956 """
957 L{_setup_boto_config} writes C{EC2_BOTO_CONFIG_FILE} with the provided
958 C{key} and C{secret} parameters.
959 """
960 key = "my_ec2_key"
961 secret = "my_secret_access_key"
962 expected = ["[Credentials]",
963 "aws_access_key_id = %s" % key,
964 "aws_secret_access_key = %s" % secret]
965 self.storage._setup_boto_config(key=key, secret=secret)
966 with open(util.EC2_BOTO_CONFIG_FILE) as boto_config:
967 lines = boto_config.read().splitlines()
968 self.assertEqual(lines, expected)
969
944 def test_load_environment_with_ec2_variables(self):970 def test_load_environment_with_ec2_variables(self):
945 """971 """
946 L{load_environment} will setup script environment variables for ec2972 L{load_environment} will setup script environment variables for ec2
@@ -948,8 +974,7 @@
948 variables and then call L{validate_credentials} to assert974 variables and then call L{validate_credentials} to assert
949 that environment variables provided give access to the service.975 that environment variables provided give access to the service.
950 """976 """
951 self.addCleanup(setattr, util.os, "environ", util.os.environ)977 self._set_environment_vars({})
952 util.os.environ = {}
953978
954 def mock_validate():979 def mock_validate():
955 pass980 pass
@@ -959,7 +984,7 @@
959 expected = {984 expected = {
960 "EC2_ACCESS_KEY": "ec2key",985 "EC2_ACCESS_KEY": "ec2key",
961 "EC2_SECRET_KEY": "ec2password",986 "EC2_SECRET_KEY": "ec2password",
962 "EC2_URL": "https://ec2-region-url:443/v2.0/"987 "EC2_URL": "https://ec2-region-1.com"
963 }988 }
964 self.assertEqual(util.os.environ, expected)989 self.assertEqual(util.os.environ, expected)
965990
@@ -968,45 +993,28 @@
968 L{load_environment} will exit in failure and log a message if any993 L{load_environment} will exit in failure and log a message if any
969 required configuration option is not set.994 required configuration option is not set.
970 """995 """
971 self.addCleanup(setattr, util.os, "environ", util.os.environ)
972
973 def mock_validate():996 def mock_validate():
974 raise SystemExit("something invalid")997 raise SystemExit("something invalid")
975 self.storage.validate_credentials = mock_validate998 self.storage.validate_credentials = mock_validate
976999
977 self.assertRaises(SystemExit, self.storage.load_environment)1000 self.assertRaises(SystemExit, self.storage.load_environment)
9781001
979 def test_validate_credentials_failure(self):
980 """
981 L{validate_credentials} will attempt a simple euca command to ensure
982 the environment is properly configured to access the nova service.
983 Upon failure to contact the nova service, L{validate_credentials} will
984 exit in error and log a message.
985 """
986 command = "euca-describe-instances"
987 nova_cmd = self.mocker.replace(subprocess.check_call)
988 nova_cmd(command, shell=True)
989 self.mocker.throw(subprocess.CalledProcessError(1, command))
990 self.mocker.replay()
991
992 result = self.assertRaises(
993 SystemExit, self.storage.validate_credentials)
994 self.assertEqual(result.code, 1)
995 message = (
996 "ERROR: Charm configured credentials can't access endpoint. "
997 "Command '%s' returned non-zero exit status 1" % command)
998 self.assertIn(
999 message, util.hookenv._log_ERROR, "Not logged- %s" % message)
1000
1001 def test_validate_credentials(self):1002 def test_validate_credentials(self):
1002 """1003 """
1003 L{validate_credentials} will succeed when a simple euca command1004 L{validate_credentials} will succeed when a boto's L{get_all_volumes}
1004 succeeds due to a properly configured environment based on the charm1005 succeeds using C{EC2_URL} environment variable from charm configuration
1005 configuration options.1006 options.
1006 """1007 """
1007 command = "euca-describe-instances"1008 ec2_url = "https://ec2.us-west-1.amazonaws.com"
1008 nova_cmd = self.mocker.replace(subprocess.check_call)1009 self._set_environment_vars({"EC2_URL": ec2_url})
1009 nova_cmd(command, shell=True)1010 connect = self.mocker.replace("boto.ec2.connect_to_region")
1011 connect("us-west-1")
1012
1013 def get_volumes():
1014 return []
1015
1016 self.mocker.result(MockBotoEC2Connection(
1017 get_volumes=get_volumes))
1010 self.mocker.replay()1018 self.mocker.replay()
10111019
1012 self.storage.validate_credentials()1020 self.storage.validate_credentials()
@@ -1040,9 +1048,7 @@
1040 labelled with the os.environ[JUJU_REMOTE_UNIT].1048 labelled with the os.environ[JUJU_REMOTE_UNIT].
1041 """1049 """
1042 unit_name = "postgresql/0"1050 unit_name = "postgresql/0"
1043 self.addCleanup(1051 self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name})
1044 setattr, os, "environ", os.environ)
1045 os.environ = {"JUJU_REMOTE_UNIT": unit_name}
1046 volume_id = "123134124-1241412-1242141"1052 volume_id = "123134124-1241412-1242141"
10471053
1048 def mock_describe(val):1054 def mock_describe(val):
@@ -1061,9 +1067,7 @@
1061 L{_ec2_describe_volumes} for the os.environ[JUJU_REMOTE_UNIT].1067 L{_ec2_describe_volumes} for the os.environ[JUJU_REMOTE_UNIT].
1062 """1068 """
1063 unit_name = "postgresql/0"1069 unit_name = "postgresql/0"
1064 self.addCleanup(1070 self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name})
1065 setattr, os, "environ", os.environ)
1066 os.environ = {"JUJU_REMOTE_UNIT": unit_name}
10671071
1068 def mock_describe(val):1072 def mock_describe(val):
1069 self.assertIsNone(val)1073 self.assertIsNone(val)
@@ -1082,8 +1086,7 @@
1082 multiple results the function exits with an error.1086 multiple results the function exits with an error.
1083 """1087 """
1084 unit_name = "postgresql/0"1088 unit_name = "postgresql/0"
1085 self.addCleanup(setattr, os, "environ", os.environ)1089 self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name})
1086 os.environ = {"JUJU_REMOTE_UNIT": unit_name}
10871090
1088 def mock_describe(val):1091 def mock_describe(val):
1089 self.assertIsNone(val)1092 self.assertIsNone(val)
@@ -1109,8 +1112,7 @@
1109 unit_name = "postgresql/0"1112 unit_name = "postgresql/0"
1110 instance_id = "i-123123"1113 instance_id = "i-123123"
1111 volume_id = "123-123-123"1114 volume_id = "123-123-123"
1112 self.addCleanup(setattr, os, "environ", os.environ)1115 self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name})
1113 os.environ = {"JUJU_REMOTE_UNIT": unit_name}
11141116
1115 self.storage.load_environment = lambda: None1117 self.storage.load_environment = lambda: None
1116 self.storage._ec2_describe_volumes = lambda volume_id: {}1118 self.storage._ec2_describe_volumes = lambda volume_id: {}
@@ -1134,8 +1136,7 @@
1134 volume_id = "123-123-123"1136 volume_id = "123-123-123"
1135 instance_id = "i-123123123"1137 instance_id = "i-123123123"
1136 volume_label = "%s unit volume" % unit_name1138 volume_label = "%s unit volume" % unit_name
1137 self.addCleanup(setattr, os, "environ", os.environ)1139 self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name})
1138 os.environ = {"JUJU_REMOTE_UNIT": unit_name}
1139 self.storage.load_environment = lambda: None1140 self.storage.load_environment = lambda: None
11401141
1141 def mock_get_volume_id(label):1142 def mock_get_volume_id(label):
@@ -1163,8 +1164,7 @@
1163 unit_name = "postgresql/0"1164 unit_name = "postgresql/0"
1164 instance_id = "i-123123"1165 instance_id = "i-123123"
1165 volume_id = "123-123-123"1166 volume_id = "123-123-123"
1166 self.addCleanup(setattr, os, "environ", os.environ)1167 self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name})
1167 os.environ = {"JUJU_REMOTE_UNIT": unit_name}
11681168
1169 self.storage.load_environment = lambda: None1169 self.storage.load_environment = lambda: None
11701170
@@ -1188,8 +1188,7 @@
1188 unit_name = "postgresql/0"1188 unit_name = "postgresql/0"
1189 instance_id = "i-123123"1189 instance_id = "i-123123"
1190 volume_id = "123-123-123"1190 volume_id = "123-123-123"
1191 self.addCleanup(setattr, os, "environ", os.environ)1191 self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name})
1192 os.environ = {"JUJU_REMOTE_UNIT": unit_name}
11931192
1194 self.storage.load_environment = lambda: None1193 self.storage.load_environment = lambda: None
11951194
@@ -1217,8 +1216,7 @@
1217 volume_id = "123-123-123"1216 volume_id = "123-123-123"
1218 volume_label = "%s unit volume" % unit_name1217 volume_label = "%s unit volume" % unit_name
1219 default_volume_size = util.hookenv.config("default_volume_size")1218 default_volume_size = util.hookenv.config("default_volume_size")
1220 self.addCleanup(setattr, os, "environ", os.environ)1219 self._set_environment_vars({"JUJU_REMOTE_UNIT": unit_name})
1221 os.environ = {"JUJU_REMOTE_UNIT": unit_name}
12221220
1223 self.storage.load_environment = lambda: None1221 self.storage.load_environment = lambda: None
1224 self.storage.get_volume_id = lambda _: None1222 self.storage.get_volume_id = lambda _: None
@@ -1240,26 +1238,116 @@
1240 self.assertIn(1238 self.assertIn(
1241 message, util.hookenv._log_INFO, "Not logged- %s" % message)1239 message, util.hookenv._log_INFO, "Not logged- %s" % message)
12421240
1243 def test_wb_ec2_describe_volumes_command_error(self):1241 def test_wb_ec2_validate_credentials_invalid_ec2_url_environment_var(self):
1244 """1242 """
1245 L{_ec2_describe_volumes} will exit in error when the euca2ools1243 L{_ec2_validate_credentials} will exit in error when the C{EC2_URL}
1246 C{DescribeVolumes} command fails.1244 environment variable is invalid.
1247 """1245 """
1248 euca_command = self.mocker.replace(self.storage.ec2_volume_class)1246 self._set_environment_vars({"EC2_URL": "not-a-valid-ec2-url"})
1249 euca_command()1247
1250 self.mocker.throw(SystemExit(1))1248 result = self.assertRaises(
1251 self.mocker.replay()1249 SystemExit, self.storage._ec2_validate_credentials)
12521250 self.assertEqual(result.code, 1)
1253 result = self.assertRaises(1251 message = (
1254 SystemExit, self.storage._ec2_describe_volumes)1252 "ERROR: Couldn't get region from EC2_URL environment variable: "
1255 self.assertEqual(result.code, 1)1253 "not-a-valid-ec2-url.")
1256 message = "ERROR: Couldn't contact EC2 using euca-describe-volumes"1254 self.assertIn(
1255 message, util.hookenv._log_ERROR, "Not logged- %s" % message)
1256
1257 def test_wb_ec2_validate_credentials_absent_ec2_url_environment_var(self):
1258 """
1259 L{_ec2_validate_credentials} will exit in error when the C{EC2_URL}
1260 environment variable is not present.
1261 """
1262 self._set_environment_vars({})
1263
1264 result = self.assertRaises(
1265 SystemExit, self.storage._ec2_validate_credentials)
1266 self.assertEqual(result.code, 1)
1267 message = (
1268 "ERROR: Couldn't get region from EC2_URL environment variable: "
1269 "NOT SET.")
1270 self.assertIn(
1271 message, util.hookenv._log_ERROR, "Not logged- %s" % message)
1272
1273 def test_wb_ec2_validate_credentials_unavailable_credentials(self):
1274 """
1275 L{_ec2_validate_credentials} will exit in error when the EC2
1276 credentials are not present in /etc/boto.cfg.
1277 """
1278 self._set_environment_vars(
1279 {"EC2_URL": "https://ec2.us-west-1.amazonaws.com"})
1280 connect = self.mocker.replace("boto.ec2.connect_to_region")
1281 connect("us-west-1")
1282
1283 def get_volumes_error():
1284 raise NoAuthHandlerFound("No handler was ready to auth.")
1285
1286 self.mocker.result(
1287 MockBotoEC2Connection(get_volumes=get_volumes_error))
1288 self.mocker.replay()
1289
1290 result = self.assertRaises(
1291 SystemExit, self.storage._ec2_validate_credentials)
1292 self.assertEqual(result.code, 1)
1293 message = (
1294 "ERROR: EC2 credentials not found in /etc/boto.cfg. Cannot "
1295 "authenticate.")
1296 self.assertIn(
1297 message, util.hookenv._log_ERROR, "Not logged- %s" % message)
1298
1299 def test_wb_ec2_validate_credentials_invalid_credentials(self):
1300 """
1301 L{_ec2_validate_credentials} will exit in error when the EC2
1302 credentials are not valid for the C{EC2_URL} provided.
1303 """
1304 self._set_environment_vars(
1305 {"EC2_URL": "https://ec2.us-west-1.amazonaws.com"})
1306 connect = self.mocker.replace("boto.ec2.connect_to_region")
1307 connect("us-west-1")
1308
1309 def get_volumes_error():
1310 raise EC2ResponseError(401, "Unauthorized")
1311
1312 self.mocker.result(
1313 MockBotoEC2Connection(get_volumes=get_volumes_error))
1314 self.mocker.replay()
1315
1316 result = self.assertRaises(
1317 SystemExit, self.storage._ec2_validate_credentials)
1318 self.assertEqual(result.code, 1)
1319 message = (
1320 "ERROR: Invalid EC2 credentials in /etc/boto.cfg. Unauthorized.")
1321 self.assertIn(
1322 message, util.hookenv._log_ERROR, "Not logged- %s" % message)
1323
1324 def test_wb_ec2_validate_credentials_timeout(self):
1325 """
1326 L{_ec2_validate_credentials} will exit in error when a connection
1327 timeout occurs against the region endpoint.
1328 """
1329 ec2_url = "https://ec2.us-west-1.amazonaws.com"
1330 self._set_environment_vars({"EC2_URL": ec2_url})
1331 connect = self.mocker.replace("boto.ec2.connect_to_region")
1332 connect("us-west-1")
1333
1334 def get_volumes_error():
1335 raise gaierror("Temporary failure in name resolution")
1336
1337 self.mocker.result(
1338 MockBotoEC2Connection(get_volumes=get_volumes_error))
1339 self.mocker.replay()
1340
1341 result = self.assertRaises(
1342 SystemExit, self.storage._ec2_validate_credentials)
1343 self.assertEqual(result.code, 1)
1344 message = "ERROR: Connectivity error accessing %s" % ec2_url
1257 self.assertIn(1345 self.assertIn(
1258 message, util.hookenv._log_ERROR, "Not logged- %s" % message)1346 message, util.hookenv._log_ERROR, "Not logged- %s" % message)
12591347
1260 def test_wb_ec2_describe_volumes_without_attached_instances(self):1348 def test_wb_ec2_describe_volumes_without_attached_instances(self):
1261 """1349 """
1262 L{_ec2_describe_volumes} parses the results of euca2ools1350 L{_ec2_describe_volumes} parses the results of boto.ec2.get_all_volumes
1263 C{DescribeVolumes} to create a C{dict} of volume information. When no1351 C{DescribeVolumes} to create a C{dict} of volume information. When no
1264 C{instance_id}s are present the volumes are not attached so no1352 C{instance_id}s are present the volumes are not attached so no
1265 C{device} or C{instance_id} information will be present.1353 C{device} or C{instance_id} information will be present.
@@ -1272,10 +1360,11 @@
1272 "456-456-456", device="/dev/notshown", instance_id="notseen",1360 "456-456-456", device="/dev/notshown", instance_id="notseen",
1273 zone="ec2-az2", size="8", status="available",1361 zone="ec2-az2", size="8", status="available",
1274 snapshot_id="some-shot", tags={"volume_name": "my volume name"})1362 snapshot_id="some-shot", tags={"volume_name": "my volume name"})
1275 euca_command = self.mocker.replace(self.storage.ec2_volume_class)1363
1276 euca_command()1364 def get_volumes():
1277 self.mocker.result(MockEucaCommand([volume1, volume2]))1365 return [volume1, volume2]
1278 self.mocker.replay()1366
1367 self.storage.ec2_conn = MockBotoEC2Connection(get_volumes=get_volumes)
12791368
1280 expected = {"123-123-123": {"id": "123-123-123", "status": "available",1369 expected = {"123-123-123": {"id": "123-123-123", "status": "available",
1281 "device": "",1370 "device": "",
@@ -1295,9 +1384,8 @@
12951384
1296 def test_wb_ec2_describe_volumes_matches_volume_id_supplied(self):1385 def test_wb_ec2_describe_volumes_matches_volume_id_supplied(self):
1297 """1386 """
1298 L{_ec2_describe_volumes} parses the results of euca2ools1387 L{_ec2_describe_volumes} matches the provided C{volume_id} to the
1299 C{DescribeVolumes} to create a C{dict} of volume information.1388 results of boto's L{get_all_volumes}.
1300 When C{volume_id} is provided return a C{dict} for the matched volume.
1301 """1389 """
1302 volume_id = "123-123-123"1390 volume_id = "123-123-123"
1303 volume1 = MockVolume(1391 volume1 = MockVolume(
@@ -1308,10 +1396,11 @@
1308 "456-456-456", device="/dev/notshown", instance_id="notseen",1396 "456-456-456", device="/dev/notshown", instance_id="notseen",
1309 zone="ec2-az2", size="8", status="available",1397 zone="ec2-az2", size="8", status="available",
1310 snapshot_id="some-shot", tags={"volume_name": "my volume name"})1398 snapshot_id="some-shot", tags={"volume_name": "my volume name"})
1311 euca_command = self.mocker.replace(self.storage.ec2_volume_class)1399
1312 euca_command()1400 def get_volumes():
1313 self.mocker.result(MockEucaCommand([volume1, volume2]))1401 return [volume1, volume2]
1314 self.mocker.replay()1402
1403 self.storage.ec2_conn = MockBotoEC2Connection(get_volumes=get_volumes)
13151404
1316 expected = {1405 expected = {
1317 "id": volume_id, "status": "available", "device": "",1406 "id": volume_id, "status": "available", "device": "",
@@ -1323,29 +1412,28 @@
13231412
1324 def test_wb_ec2_describe_volumes_unmatched_volume_id_supplied(self):1413 def test_wb_ec2_describe_volumes_unmatched_volume_id_supplied(self):
1325 """1414 """
1326 L{_ec2_describe_volumes} parses the results of euca2ools1415 L{_ec2_describe_volumes} returns an empty C{dict} when it does not
1327 C{DescribeVolumes} to create a C{dict} of volume information.1416 match the provided C{volume_id} to any volumes reported by boto's
1328 When C{volume_id} is provided and unmatched, return an empty C{dict}.1417 L{get_all_volumes} method.
1329 """1418 """
1330 unmatched_volume_id = "456-456-456"1419 unmatched_volume_id = "456-456-456"
1331 volume1 = MockVolume(1420 volume1 = MockVolume(
1332 "123-123-123", device="/dev/notshown", instance_id="notseen",1421 "123-123-123", device="/dev/notshown", instance_id="notseen",
1333 zone="ec2-az1", size="10", status="available",1422 zone="ec2-az1", size="10", status="available",
1334 snapshot_id="some-shot", tags={})1423 snapshot_id="some-shot", tags={})
1335 euca_command = self.mocker.replace(self.storage.ec2_volume_class)1424
1336 euca_command()1425 def get_volumes():
1337 self.mocker.result(MockEucaCommand([volume1]))1426 return [volume1]
1338 self.mocker.replay()1427
1428 self.storage.ec2_conn = MockBotoEC2Connection(get_volumes=get_volumes)
13391429
1340 self.assertEqual(1430 self.assertEqual(
1341 self.storage._ec2_describe_volumes(unmatched_volume_id), {})1431 self.storage._ec2_describe_volumes(unmatched_volume_id), {})
13421432
1343 def test_wb_ec2_describe_volumes_with_attached_instances(self):1433 def test_wb_ec2_describe_volumes_with_attached_instances(self):
1344 """1434 """
1345 L{_ec2_describe_volumes} parses the results of euca2ools1435 L{_ec2_describe_volumes} will report attached instance_id information
1346 C{DescribeVolumes} to create a C{dict} of volume information. If1436 when boto's L{get_all_volumes} lists instance information for a volume.
1347 C{status} is C{in-use}, both C{device} and C{instance_id} will be
1348 returned in the C{dict}.
1349 """1437 """
1350 volume1 = MockVolume(1438 volume1 = MockVolume(
1351 "123-123-123", device="/dev/notshown", instance_id="notseen",1439 "123-123-123", device="/dev/notshown", instance_id="notseen",
@@ -1355,10 +1443,11 @@
1355 "456-456-456", device="/dev/xvdc", instance_id="i-456456",1443 "456-456-456", device="/dev/xvdc", instance_id="i-456456",
1356 zone="ec2-az2", size="8", status="in-use",1444 zone="ec2-az2", size="8", status="in-use",
1357 snapshot_id="some-shot", tags={"volume_name": "my volume name"})1445 snapshot_id="some-shot", tags={"volume_name": "my volume name"})
1358 euca_command = self.mocker.replace(self.storage.ec2_volume_class)1446
1359 euca_command()1447 def get_volumes():
1360 self.mocker.result(MockEucaCommand([volume1, volume2]))1448 return [volume1, volume2]
1361 self.mocker.replay()1449
1450 self.storage.ec2_conn = MockBotoEC2Connection(get_volumes=get_volumes)
13621451
1363 expected = {"123-123-123": {"id": "123-123-123", "status": "available",1452 expected = {"123-123-123": {"id": "123-123-123", "status": "available",
1364 "device": "",1453 "device": "",
@@ -1379,41 +1468,38 @@
13791468
1380 def test_wb_ec2_create_volume(self):1469 def test_wb_ec2_create_volume(self):
1381 """1470 """
1382 L{_ec2_create_volume} uses the command C{euca-create-volume} to create1471 L{_ec2_create_volume} calls boto's L{create_volume} to create
1383 a volume. It determines the availability zone for the volume by1472 a volume. It determines the availability zone for the volume by
1384 querying L{_ec2_describe_instances} on the provided C{instance_id}1473 querying L{_ec2_describe_instances} for the provided C{instance_id}
1385 to ensure it matches the same availablity zone. It will then call1474 to ensure the volume is created in the same availablity zone.
1386 L{_ec2_create_tag} to setup the C{volume_name} tag for the volume.1475 L{_ec2_create_volume} will also call L{_ec2_create_tag} to setup the
1476 C{volume_name} tag for the volume.
1387 """1477 """
1388 instance_id = "i-123123"1478 instance_id = "i-123123"
1389 volume_id = "123-123-123"1479 volume_id = "123-123-123"
1390 volume_label = "postgresql/0 unit volume"1480 volume_label = "postgresql/0 unit volume"
1391 size = 101481 size = 10
1392 zone = "ec2-az3"1482 zone = "ec2-az3"
1393 command = "euca-create-volume -z %s -s %s" % (zone, size)
13941483
1395 reservation = MockEucaReservation(1484 reservation = MockEucaReservation(
1396 [MockEucaInstance(1485 [MockEucaInstance(
1397 instance_id=instance_id, availability_zone=zone)])1486 instance_id=instance_id, availability_zone=zone)])
1398 euca_command = self.mocker.replace(self.storage.ec2_instance_class)1487
1399 euca_command()1488 def get_instances():
1400 self.mocker.result(MockEucaCommand([reservation]))1489 return [reservation]
1401 create = self.mocker.replace(subprocess.check_output)1490
1402 create(command, shell=True)1491 def create_volume(size, zone):
1403 self.mocker.result(1492 self.assertEqual(zone, "ec2-az3")
1404 "VOLUME %s 9 nova creating 2014-03-14T17:26:20\n" % volume_id)1493 self.assertEqual(size, 10)
1405 self.mocker.replay()1494 return MockVolume(vol_id=volume_id)
14061495
1407 def mock_describe_volumes(my_id):1496 def create_tags(resource_ids, tags):
1408 self.assertEqual(my_id, volume_id)1497 self.assertEqual(resource_ids, [volume_id])
1409 return {"id": volume_id}1498 self.assertEqual({"volume_name": volume_label}, tags)
1410 self.storage._ec2_describe_volumes = mock_describe_volumes1499
14111500 self.storage.ec2_conn = MockBotoEC2Connection(
1412 def mock_create_tag(my_id, key, value):1501 get_instances=get_instances, create_volume=create_volume,
1413 self.assertEqual(my_id, volume_id)1502 create_tags=create_tags)
1414 self.assertEqual(key, "volume_name")
1415 self.assertEqual(value, volume_label)
1416 self.storage._ec2_create_tag = mock_create_tag
14171503
1418 self.assertEqual(1504 self.assertEqual(
1419 self.storage._ec2_create_volume(size, volume_label, instance_id),1505 self.storage._ec2_create_volume(size, volume_label, instance_id),
@@ -1436,10 +1522,11 @@
1436 volume_label = "postgresql/0 unit volume"1522 volume_label = "postgresql/0 unit volume"
1437 size = 101523 size = 10
14381524
1439 euca_command = self.mocker.replace(self.storage.ec2_instance_class)1525 def get_instances():
1440 euca_command()1526 return []
1441 self.mocker.result(MockEucaCommand([])) # Empty results from euca1527
1442 self.mocker.replay()1528 self.storage.ec2_conn = MockBotoEC2Connection(
1529 get_instances=get_instances)
14431530
1444 result = self.assertRaises(1531 result = self.assertRaises(
1445 SystemExit, self.storage._ec2_create_volume, size, volume_label,1532 SystemExit, self.storage._ec2_create_volume, size, volume_label,
@@ -1448,174 +1535,85 @@
1448 config = util.hookenv.config_get()1535 config = util.hookenv.config_get()
1449 message = (1536 message = (
1450 "ERROR: Could not create volume for instance %s. No instance "1537 "ERROR: Could not create volume for instance %s. No instance "
1451 "details discovered by euca-describe-instances. Maybe the "1538 "details discovered by boto.ec2.get_all_instances. Maybe the "
1452 "charm configured endpoint %s is not valid for this region." %1539 "charm configured endpoint %s is not valid for this region." %
1453 (instance_id, config["endpoint"]))1540 (instance_id, config["endpoint"]))
1454 self.assertIn(1541 self.assertIn(
1455 message, util.hookenv._log_ERROR, "Not logged- %s" % message)1542 message, util.hookenv._log_ERROR, "Not logged- %s" % message)
14561543
1457 def test_wb_ec2_create_volume_error_invalid_response_type(self):1544 def test_wb_ec2_create_volume_error(self):
1458 """1545 """
1459 L{_ec2_create_volume} will log an error and exit when it receives an1546 L{_ec2_create_volume} will log an error and exit when it receives an
1460 unparseable response type from the C{euca-create-volume} command.1547 exception from boto's C{create_volume} command.
1461 """1548 """
1462 instance_id = "i-123123"1549 instance_id = "i-123123"
1463 volume_label = "postgresql/0 unit volume"1550 volume_label = "postgresql/0 unit volume"
1464 size = 101551 size = 10
1465 zone = "ec2-az3"1552 zone = "ec2-az3"
1466 command = "euca-create-volume -z %s -s %s" % (zone, size)1553
14671554 reservation = MockEucaReservation(
1468 reservation = MockEucaReservation(1555 [MockEucaInstance(
1469 [MockEucaInstance(1556 instance_id=instance_id, availability_zone=zone)])
1470 instance_id=instance_id, availability_zone=zone)])1557
1471 euca_command = self.mocker.replace(self.storage.ec2_instance_class)1558 def get_instances():
1472 euca_command()1559 return [reservation]
1473 self.mocker.result(MockEucaCommand([reservation]))1560
1474 create = self.mocker.replace(subprocess.check_output)1561 def create_volume(size, zone):
1475 create(command, shell=True)1562 self.assertEqual(zone, "ec2-az3")
1476 self.mocker.result("INSTANCE invalid-instance-type-response\n")1563 self.assertEqual(size, 10)
1477 self.mocker.replay()1564 raise EC2ResponseError(401, "Unauthorized")
14781565
1479 def mock_describe_volumes(my_id):1566 self.storage.ec2_conn = MockBotoEC2Connection(
1480 raise Exception("_ec2_describe_volumes should not be called")1567 get_instances=get_instances, create_volume=create_volume)
1481 self.storage._ec2_describe_volumes = mock_describe_volumes1568
14821569 result = self.assertRaises(
1483 def mock_create_tag(my_id, key, value):1570 SystemExit, self.storage._ec2_create_volume, size, volume_label,
1484 raise Exception("_ec2_create_tag should not be called")1571 instance_id)
1485 self.storage._ec2_create_tag = mock_create_tag1572 self.assertEqual(result.code, 1)
14861573 message = "ERROR: EC2ResponseError: 401 Unauthorized\n"
1487 result = self.assertRaises(
1488 SystemExit, self.storage._ec2_create_volume, size, volume_label,
1489 instance_id)
1490 self.assertEqual(result.code, 1)
1491 message = (
1492 "ERROR: Didn't get VOLUME response from euca-create-volume. "
1493 "Response: INSTANCE invalid-instance-type-response\n")
1494 self.assertIn(
1495 message, util.hookenv._log_ERROR, "Not logged- %s" % message)
1496
1497 def test_wb_ec2_create_volume_error_new_volume_not_found(self):
1498 """
1499 L{_ec2_create_volume} will log an error and exit when it cannot find
1500 details of the newly created C{volume_id} through a subsequent call to
1501 L{_ec2_descibe_volumes}.
1502 """
1503 instance_id = "i-123123"
1504 volume_id = "123-123-123"
1505 volume_label = "postgresql/0 unit volume"
1506 size = 10
1507 zone = "ec2-az3"
1508 command = "euca-create-volume -z %s -s %s" % (zone, size)
1509
1510 reservation = MockEucaReservation(
1511 [MockEucaInstance(
1512 instance_id=instance_id, availability_zone=zone)])
1513 euca_command = self.mocker.replace(self.storage.ec2_instance_class)
1514 euca_command()
1515 self.mocker.result(MockEucaCommand([reservation]))
1516 create = self.mocker.replace(subprocess.check_output)
1517 create(command, shell=True)
1518 self.mocker.result(
1519 "VOLUME %s 9 nova creating 2014-03-14T17:26:20\n" % volume_id)
1520 self.mocker.replay()
1521
1522 def mock_describe_volumes(my_id):
1523 self.assertEqual(my_id, volume_id)
1524 return {} # No details found for this volume
1525 self.storage._ec2_describe_volumes = mock_describe_volumes
1526
1527 def mock_create_tag(my_id, key, value):
1528 raise Exception("_ec2_create_tag should not be called")
1529 self.storage._ec2_create_tag = mock_create_tag
1530
1531 result = self.assertRaises(
1532 SystemExit, self.storage._ec2_create_volume, size, volume_label,
1533 instance_id)
1534 self.assertEqual(result.code, 1)
1535 message = (
1536 "ERROR: Unable to find volume '%s'" % volume_id)
1537 self.assertIn(
1538 message, util.hookenv._log_ERROR, "Not logged- %s" % message)
1539
1540 def test_wb_ec2_create_volume_error_command_failed(self):
1541 """
1542 L{_ec2_create_volume} will log an error and exit when the
1543 C{euca-create-volume} fails.
1544 """
1545 instance_id = "i-123123"
1546 volume_label = "postgresql/0 unit volume"
1547 size = 10
1548 zone = "ec2-az3"
1549 command = "euca-create-volume -z %s -s %s" % (zone, size)
1550
1551 reservation = MockEucaReservation(
1552 [MockEucaInstance(
1553 instance_id=instance_id, availability_zone=zone)])
1554 euca_command = self.mocker.replace(self.storage.ec2_instance_class)
1555 euca_command()
1556 self.mocker.result(MockEucaCommand([reservation]))
1557 create = self.mocker.replace(subprocess.check_output)
1558 create(command, shell=True)
1559 self.mocker.throw(subprocess.CalledProcessError(1, command))
1560 self.mocker.replay()
1561
1562 def mock_exception(my_id):
1563 raise Exception("These methods should not be called")
1564 self.storage._ec2_describe_volumes = mock_exception
1565 self.storage._ec2_create_tag = mock_exception
1566
1567 result = self.assertRaises(
1568 SystemExit, self.storage._ec2_create_volume, size, volume_label,
1569 instance_id)
1570 self.assertEqual(result.code, 1)
1571
1572 message = (
1573 "ERROR: Command '%s' returned non-zero exit status 1" % command)
1574 self.assertIn(1574 self.assertIn(
1575 message, util.hookenv._log_ERROR, "Not logged- %s" % message)1575 message, util.hookenv._log_ERROR, "Not logged- %s" % message)
15761576
1577 def test_wb_ec2_attach_volume(self):1577 def test_wb_ec2_attach_volume(self):
1578 """1578 """
1579 L{_ec2_attach_volume} uses the command C{euca-attach-volume} and1579 L{_ec2_attach_volume} uses the EC2 boto's C{attach-volume} and
1580 returns the attached volume path.1580 returns the attached device path.
1581 """1581 """
1582 instance_id = "i-123123"1582 instance_id = "i-123123"
1583 volume_id = "123-123-123"1583 volume_id = "123-123-123"
1584 device = "/dev/xvdc"1584 device = "/dev/xvdc"
1585 command = (1585
1586 "euca-attach-volume -i %s -d %s %s" %1586 def attach_volume(volume_id, instance_id, device):
1587 (instance_id, device, volume_id))1587 self.assertEqual(instance_id, "i-123123")
15881588 self.assertEqual(volume_id, "123-123-123")
1589 attach = self.mocker.replace(subprocess.check_call)1589 self.assertEqual(device, "/dev/xvdc")
1590 attach(command, shell=True)1590
1591 self.mocker.replay()1591 self.storage.ec2_conn = MockBotoEC2Connection(
1592 attach_volume=attach_volume)
15921593
1593 self.assertEqual(1594 self.assertEqual(
1594 self.storage._ec2_attach_volume(instance_id, volume_id),1595 self.storage._ec2_attach_volume(instance_id, volume_id),
1595 device)1596 device)
15961597
1597 def test_wb_ec2_attach_volume_command_failed(self):1598 def test_wb_ec2_attach_volume_failed(self):
1598 """1599 """
1599 L{_ec2_attach_volume} exits in error when C{euca-attach-volume} fails.1600 L{_ec2_attach_volume} exits in error when boto's C{attach-volume}
1601 fails.
1600 """1602 """
1601 instance_id = "i-123123"1603 instance_id = "i-123123"
1602 volume_id = "123-123-123"1604 volume_id = "123-123-123"
1603 device = "/dev/xvdc"1605
1604 command = (1606 def attach_volume(volume_id, instance_id, device):
1605 "euca-attach-volume -i %s -d %s %s" %1607 raise EC2ResponseError(401, "Unauthorized")
1606 (instance_id, device, volume_id))1608
16071609 self.storage.ec2_conn = MockBotoEC2Connection(
1608 attach = self.mocker.replace(subprocess.check_call)1610 attach_volume=attach_volume)
1609 attach(command, shell=True)
1610 self.mocker.throw(subprocess.CalledProcessError(1, command))
1611 self.mocker.replay()
16121611
1613 result = self.assertRaises(1612 result = self.assertRaises(
1614 SystemExit, self.storage._ec2_attach_volume, instance_id,1613 SystemExit, self.storage._ec2_attach_volume, instance_id,
1615 volume_id)1614 volume_id)
1616 self.assertEqual(result.code, 1)1615 self.assertEqual(result.code, 1)
1617 message = (1616 message = "ERROR: EC2ResponseError: 401 Unauthorized\n"
1618 "ERROR: Command '%s' returned non-zero exit status 1" % command)
1619 self.assertIn(1617 self.assertIn(
1620 message, util.hookenv._log_ERROR, "Not logged- %s" % message)1618 message, util.hookenv._log_ERROR, "Not logged- %s" % message)
16211619
@@ -1661,46 +1659,11 @@
1661 self.assertIn(1659 self.assertIn(
1662 message, util.hookenv._log_INFO, "Not logged- %s" % message)1660 message, util.hookenv._log_INFO, "Not logged- %s" % message)
16631661
1664 def test_detach_volume_command_error(self):
1665 """
1666 When the C{euca-detach-volume} command fails, L{detach_volume} will
1667 log a message and exit in error.
1668 """
1669 volume_label = "postgresql/0 unit volume"
1670 volume_id = "123-123-123"
1671 instance_id = "i-123123"
1672 self.storage.load_environment = lambda: None
1673
1674 def mock_get_volume_id(label):
1675 self.assertEqual(label, volume_label)
1676 return volume_id
1677 self.storage.get_volume_id = mock_get_volume_id
1678
1679 def mock_describe_volumes(my_id):
1680 self.assertEqual(my_id, volume_id)
1681 return {"status": "in-use", "instance_id": instance_id}
1682 self.storage.describe_volumes = mock_describe_volumes
1683
1684 command = "euca-detach-volume -i %s %s" % (instance_id, volume_id)
1685 ec2_cmd = self.mocker.replace(subprocess.check_call)
1686 ec2_cmd(command, shell=True)
1687 self.mocker.throw(subprocess.CalledProcessError(1, command))
1688 self.mocker.replay()
1689
1690 result = self.assertRaises(
1691 SystemExit, self.storage.detach_volume, volume_label)
1692 self.assertEqual(result.code, 1)
1693 message = (
1694 "ERROR: Couldn't detach volume. Command '%s' returned non-zero "
1695 "exit status 1" % command)
1696 self.assertIn(
1697 message, util.hookenv._log_ERROR, "Not logged- %s" % message)
1698
1699 def test_detach_volume(self):1662 def test_detach_volume(self):
1700 """1663 """
1701 When L{get_volume_id} finds a volume associated with this instance1664 When L{get_volume_id} finds a volume associated with this instance
1702 which has a volume state not equal to C{available}, it detaches that1665 which has a volume state not equal to C{available}, it detaches that
1703 volume using euca2ools commands.1666 volume using boto's L{detach_volume}.
1704 """1667 """
1705 volume_label = "postgresql/0 unit volume"1668 volume_label = "postgresql/0 unit volume"
1706 volume_id = "123-123-123"1669 volume_id = "123-123-123"
@@ -1717,10 +1680,12 @@
1717 return {"status": "in-use", "instance_id": instance_id}1680 return {"status": "in-use", "instance_id": instance_id}
1718 self.storage.describe_volumes = mock_describe_volumes1681 self.storage.describe_volumes = mock_describe_volumes
17191682
1720 command = "euca-detach-volume -i %s %s" % (instance_id, volume_id)1683 def detach_volume(instance_id, volume_id):
1721 ec2_cmd = self.mocker.replace(subprocess.check_call)1684 self.assertEqual(instance_id, "i-123123")
1722 ec2_cmd(command, shell=True)1685 self.assertEqual(volume_id, "123-123-123")
1723 self.mocker.replay()1686
1687 self.storage.ec2_conn = MockBotoEC2Connection(
1688 detach_volume=detach_volume)
17241689
1725 self.storage.detach_volume(volume_label)1690 self.storage.detach_volume(volume_label)
1726 message = (1691 message = (
@@ -1728,3 +1693,38 @@
1728 (volume_id, instance_id))1693 (volume_id, instance_id))
1729 self.assertIn(1694 self.assertIn(
1730 message, util.hookenv._log_INFO, "Not logged- %s" % message)1695 message, util.hookenv._log_INFO, "Not logged- %s" % message)
1696
1697 def test_detach_volume_error(self):
1698 """
1699 An error is logged and we exit(1) when boto's L{detach_volume} raises
1700 an error.
1701 """
1702 volume_label = "postgresql/0 unit volume"
1703 volume_id = "123-123-123"
1704 instance_id = "i-123123"
1705 self.storage.load_environment = lambda: None
1706
1707 def mock_get_volume_id(label):
1708 self.assertEqual(label, volume_label)
1709 return volume_id
1710 self.storage.get_volume_id = mock_get_volume_id
1711
1712 def mock_describe_volumes(my_id):
1713 self.assertEqual(my_id, volume_id)
1714 return {"status": "in-use", "instance_id": instance_id}
1715 self.storage.describe_volumes = mock_describe_volumes
1716
1717 def detach_volume_error(instance_id, volume_id):
1718 raise EC2ResponseError(401, "Unauthorized")
1719
1720 self.storage.ec2_conn = MockBotoEC2Connection(
1721 detach_volume=detach_volume_error)
1722
1723 result = self.assertRaises(
1724 SystemExit, self.storage.detach_volume, volume_label)
1725 self.assertEqual(result.code, 1)
1726 message = (
1727 "ERROR: Couldn't detach volume. EC2ResponseError: "
1728 "401 Unauthorized\n")
1729 self.assertIn(
1730 message, util.hookenv._log_ERROR, "Not logged- %s" % message)
17311731
=== modified file 'hooks/util.py'
--- hooks/util.py 2014-07-18 00:58:58 +0000
+++ hooks/util.py 2014-08-22 13:42:09 +0000
@@ -3,6 +3,7 @@
3from charmhelpers.core import hookenv3from charmhelpers.core import hookenv
4import subprocess4import subprocess
5import os5import os
6import re
6import sys7import sys
7from time import sleep8from time import sleep
89
@@ -17,11 +18,8 @@
17 "ec2": ["endpoint", "key", "secret"],18 "ec2": ["endpoint", "key", "secret"],
18 "nova": ["endpoint", "region", "tenant", "key", "secret"]}19 "nova": ["endpoint", "region", "tenant", "key", "secret"]}
1920
20PROVIDER_COMMANDS = {21# Use python-boto for AWS describe-volumes describe-instances
21 "ec2": {"validate": "euca-describe-instances",22EC2_BOTO_CONFIG_FILE = "/etc/boto.cfg"
22 "detach": "euca-detach-volume -i %s %s"},
23 "nova": {"validate": "nova list",
24 "detach": "nova volume-detach %s %s"}}
2523
2624
27class StorageServiceUtil(object):25class StorageServiceUtil(object):
@@ -31,7 +29,6 @@
31 provider = None29 provider = None
32 environment_map = None30 environment_map = None
33 required_config_options = None31 required_config_options = None
34 commands = None
3532
36 def __init__(self, provider):33 def __init__(self, provider):
37 self.provider = provider34 self.provider = provider
@@ -43,13 +40,8 @@
43 hookenv.ERROR)40 hookenv.ERROR)
44 sys.exit(1)41 sys.exit(1)
45 self.environment_map = ENVIRONMENT_MAP[provider]42 self.environment_map = ENVIRONMENT_MAP[provider]
46 self.commands = PROVIDER_COMMANDS[provider]
47 self.required_config_options = REQUIRED_CONFIG_OPTIONS[provider]43 self.required_config_options = REQUIRED_CONFIG_OPTIONS[provider]
48 if provider == "ec2":44 self.ec2_conn = None
49 import euca2ools.commands.euca.describevolumes as getvolumes
50 import euca2ools.commands.euca.describeinstances as getinstances
51 self.ec2_volume_class = getvolumes.DescribeVolumes
52 self.ec2_instance_class = getinstances.DescribeInstances
5345
54 def load_environment(self):46 def load_environment(self):
55 """47 """
@@ -60,18 +52,26 @@
60 for option in self.required_config_options:52 for option in self.required_config_options:
61 environment_variable = self.environment_map[option]53 environment_variable = self.environment_map[option]
62 os.environ[environment_variable] = config_data[option].strip()54 os.environ[environment_variable] = config_data[option].strip()
55 if self.provider == "ec2":
56 self._setup_boto_config(
57 os.environ.get("EC2_ACCESS_KEY", None),
58 os.environ.get("EC2_SECRET_KEY", None))
63 self.validate_credentials()59 self.validate_credentials()
6460
65 def validate_credentials(self):61 def validate_credentials(self):
62 method = getattr(self, "_%s_validate_credentials" % self.provider)
63 return method()
64
65 def _nova_validate_credentials(self):
66 """66 """
67 Attempt to contact the respective ec2 or nova volume service or exit(1)67 Attempt to contact nova volume service or exit(1)
68 """68 """
69 try:69 try:
70 subprocess.check_call(self.commands["validate"], shell=True)70 subprocess.check_call("nova list", shell=True)
71 except subprocess.CalledProcessError, e:71 except subprocess.CalledProcessError, e:
72 hookenv.log(72 hookenv.log(
73 "ERROR: Charm configured credentials can't access endpoint. "73 "ERROR: Charm configured credentials can't access nova "
74 "%s" % str(e),74 "endpoint. %s" % str(e),
75 hookenv.ERROR)75 hookenv.ERROR)
76 sys.exit(1)76 sys.exit(1)
77 hookenv.log(77 hookenv.log(
@@ -177,8 +177,8 @@
177 sleep(5)177 sleep(5)
178 if not device:178 if not device:
179 hookenv.log(179 hookenv.log(
180 "ERROR: Unable to discover device attached by "180 "ERROR: Unable to discover device attached using "
181 "euca-attach-volume",181 "python boto.ec2.attach_volume.",
182 hookenv.ERROR)182 hookenv.ERROR)
183 sys.exit(1)183 sys.exit(1)
184 return device184 return device
@@ -201,9 +201,15 @@
201 hookenv.log(201 hookenv.log(
202 "Detaching volume (%s) from instance %s" %202 "Detaching volume (%s) from instance %s" %
203 (volume_id, volume["instance_id"]))203 (volume_id, volume["instance_id"]))
204
205 method = getattr(self, "_%s_detach_volume" % self.provider)
206 return method(volume_id=volume_id, instance_id=volume["instance_id"])
207
208 def _nova_detach_volume(self, volume_id, instance_id):
209 """Detach specified C{volume_id} from the nova C{instance_id}."""
204 try:210 try:
205 subprocess.check_call(211 subprocess.check_call(
206 self.commands["detach"] % (volume["instance_id"], volume_id),212 "nova volume-detach %s %s" % (instance_id, volume_id),
207 shell=True)213 shell=True)
208 except subprocess.CalledProcessError, e:214 except subprocess.CalledProcessError, e:
209 hookenv.log(215 hookenv.log(
@@ -212,35 +218,72 @@
212 return218 return
213219
214 # EC2-specific methods220 # EC2-specific methods
215 def _ec2_create_tag(self, volume_id, tag_name, tag_value=None):221 def _setup_boto_config(self, key, secret):
222 """Write EC2 credentials to C{EC2_BOTO_CONFIG_FILE}."""
223 with open(EC2_BOTO_CONFIG_FILE, "w") as config_file:
224 config_file.write(
225 "[Credentials]\naws_access_key_id = %s\n"
226 "aws_secret_access_key = %s\n" % (key, secret))
227
228 def _ec2_validate_credentials(self):
229 """Attempt to contact EC2 storage service or exit(1)."""
230 from boto.exception import NoAuthHandlerFound, EC2ResponseError
231 import boto.ec2
232 from socket import gaierror
233 message = None
234 if self.provider == "ec2" and not self.ec2_conn:
235 # parse region out of EC2_URL environment variable
236 ec2_url = os.environ.get("EC2_URL", "NOT SET")
237 matches = re.search("\S{2}-\S+-\d", ec2_url)
238 if not matches:
239 message = (
240 "ERROR: Couldn't get region from EC2_URL environment "
241 "variable: %s." % ec2_url)
242 hookenv.log(message, hookenv.ERROR)
243 sys.exit(1)
244 ec2_region = matches.group(0)
245
246 # Create connection object, doesn't actually contact AWS
247 self.ec2_conn = boto.ec2.connect_to_region(ec2_region)
248 try:
249 # Test credentials against AWS with a simple command
250 self.ec2_conn.get_all_volumes()
251 except EC2ResponseError:
252 message = (
253 "ERROR: Invalid EC2 credentials in /etc/boto.cfg. "
254 "Unauthorized.")
255 except NoAuthHandlerFound:
256 message = (
257 "ERROR: EC2 credentials not found in /etc/boto.cfg. "
258 "Cannot authenticate.")
259 except gaierror:
260 message = "ERROR: Connectivity error accessing %s" % ec2_url
261 if message:
262 hookenv.log(message, hookenv.ERROR)
263 sys.exit(1)
264 hookenv.log(
265 "Validated charm configuration credentials have access to "
266 "block storage service")
267
268 def _ec2_create_tag(self, volume_id, tag_name, tag_value=""):
216 """Attach a tag and optional C{tag_value} to the given C{volume_id}"""269 """Attach a tag and optional C{tag_value} to the given C{volume_id}"""
217 tag_string = tag_name270 tag_string = tag_name
218 if tag_value:271 if tag_value:
219 tag_string += "=%s" % tag_value272 tag_string += "=%s" % tag_value
220 command = 'euca-create-tags %s --tag "%s"' % (volume_id, tag_string)
221
222 try:273 try:
223 subprocess.check_call(command, shell=True)274 self.ec2_conn.create_tags(
224 except subprocess.CalledProcessError, e:275 resource_ids=[volume_id], tags={tag_name: tag_value})
225 hookenv.log(276 except Exception, e:
226 "ERROR: Couldn't add tags to the resource. %s" % str(e),277 hookenv.log("ERROR: %s" % str(e), hookenv.ERROR)
227 hookenv.ERROR)
228 sys.exit(1)278 sys.exit(1)
229 hookenv.log("Tagged (%s) to %s." % (tag_string, volume_id))279 hookenv.log("Tagged (%s) to %s." % (tag_string, volume_id))
230280
231 def _ec2_describe_instances(self, instance_id=None):281 def _ec2_describe_instances(self, instance_id=None):
232 """282 """
233 Use euca2ools libraries to describe instances and return a C{dict}283 Use AWS Dev API (boto) to describe instances and return a C{dict}
234 """284 """
235 result = {}285 result = {}
236 try:286 reservations = self.ec2_conn.get_all_instances()
237 command = self.ec2_instance_class()
238 reservations = command.main()
239 except SystemExit:
240 hookenv.log(
241 "ERROR: Couldn't contact EC2 using euca-describe-instances",
242 hookenv.ERROR)
243 sys.exit(1)
244 for reservation in reservations:287 for reservation in reservations:
245 for inst in reservation.instances:288 for inst in reservation.instances:
246 result[inst.id] = {289 result[inst.id] = {
@@ -259,17 +302,10 @@
259302
260 def _ec2_describe_volumes(self, volume_id=None):303 def _ec2_describe_volumes(self, volume_id=None):
261 """304 """
262 Use euca2ools libraries to describe volumes and return a C{dict}305 Use AWS Developer API (boto) to describe volumes and return a C{dict}
263 """306 """
264 result = {}307 result = {}
265 try:308 volumes = self.ec2_conn.get_all_volumes()
266 command = self.ec2_volume_class()
267 volumes = command.main()
268 except SystemExit:
269 hookenv.log(
270 "ERROR: Couldn't contact EC2 using euca-describe-volumes",
271 hookenv.ERROR)
272 sys.exit(1)
273 for volume in volumes:309 for volume in volumes:
274 result[volume.id] = {310 result[volume.id] = {
275 "device": "",311 "device": "",
@@ -306,34 +342,19 @@
306 config_data = hookenv.config()342 config_data = hookenv.config()
307 hookenv.log(343 hookenv.log(
308 "ERROR: Could not create volume for instance %s. No instance "344 "ERROR: Could not create volume for instance %s. No instance "
309 "details discovered by euca-describe-instances. Maybe the "345 "details discovered by boto.ec2.get_all_instances. Maybe the "
310 "charm configured endpoint %s is not valid for this region." %346 "charm configured endpoint %s is not valid for this region." %
311 (instance_id, config_data["endpoint"]), hookenv.ERROR)347 (instance_id, config_data["endpoint"]), hookenv.ERROR)
312 sys.exit(1)348 sys.exit(1)
313349
314 try:350 try:
315 output = subprocess.check_output(351 volume = self.ec2_conn.create_volume(
316 "euca-create-volume -z %s -s %s" %352 size=size, zone=instance["availability_zone"])
317 (instance["availability_zone"], size), shell=True)353 except Exception, e:
318 except subprocess.CalledProcessError, e:
319 hookenv.log("ERROR: %s" % str(e), hookenv.ERROR)354 hookenv.log("ERROR: %s" % str(e), hookenv.ERROR)
320 sys.exit(1)355 sys.exit(1)
321356 self._ec2_create_tag(volume.id, "volume_name", volume_label)
322 response_type, volume_id = output.split()[:2]357 return volume.id
323 if response_type != "VOLUME":
324 hookenv.log(
325 "ERROR: Didn't get VOLUME response from euca-create-volume. "
326 "Response: %s" % output, hookenv.ERROR)
327 sys.exit(1)
328 volume = self.describe_volumes(volume_id.strip())
329 if not volume:
330 hookenv.log(
331 "ERROR: Unable to find volume '%s'" % volume_id.strip(),
332 hookenv.ERROR)
333 sys.exit(1)
334 volume_id = volume["id"]
335 self._ec2_create_tag(volume_id, "volume_name", volume_label)
336 return volume_id
337358
338 def _ec2_attach_volume(self, instance_id, volume_id):359 def _ec2_attach_volume(self, instance_id, volume_id):
339 """360 """
@@ -342,14 +363,25 @@
342 """363 """
343 device = "/dev/xvdc"364 device = "/dev/xvdc"
344 try:365 try:
345 subprocess.check_call(366 self.ec2_conn.attach_volume(
346 "euca-attach-volume -i %s -d %s %s" %367 volume_id=volume_id, instance_id=instance_id, device=device)
347 (instance_id, device, volume_id), shell=True)368 except Exception, e:
348 except subprocess.CalledProcessError, e:
349 hookenv.log("ERROR: %s" % str(e), hookenv.ERROR)369 hookenv.log("ERROR: %s" % str(e), hookenv.ERROR)
350 sys.exit(1)370 sys.exit(1)
351 return device371 return device
352372
373 def _ec2_detach_volume(self, instance_id, volume_id):
374 """
375 Detach an EC2 C{volume_id} from the provided C{instance_id}.
376 """
377 try:
378 self.ec2_conn.detach_volume(
379 volume_id=volume_id, instance_id=instance_id)
380 except Exception, e:
381 hookenv.log(
382 "ERROR: Couldn't detach volume. %s" % str(e), hookenv.ERROR)
383 sys.exit(1)
384
353 # Nova-specific methods385 # Nova-specific methods
354 def _nova_volume_show(self, volume_id):386 def _nova_volume_show(self, volume_id):
355 """387 """

Subscribers

People subscribed via source and target branches

to all changes: