Merge lp:~chad.smith/charms/precise/block-storage-broker/bsb-trusty-support into lp:~chad.smith/charms/precise/block-storage-broker/trunk

Proposed by Chad Smith
Status: Rejected
Rejected by: Chad Smith
Proposed branch: lp:~chad.smith/charms/precise/block-storage-broker/bsb-trusty-support
Merge into: lp:~chad.smith/charms/precise/block-storage-broker/trunk
Prerequisite: lp:~chad.smith/charms/precise/block-storage-broker/bsb-ec2-support
Diff against target: 1107 lines (+602/-255)
2 files modified
hooks/test_util.py (+394/-143)
hooks/util.py (+208/-112)
To merge this branch: bzr merge lp:~chad.smith/charms/precise/block-storage-broker/bsb-trusty-support
Reviewer Review Type Date Requested Status
Chad Smith Disapprove
Alberto Donato (community) Needs Fixing
Review via email: mp+211963@code.launchpad.net

Description of the change

This branch does a couple things:
 1. stops the block-storage-broker from importing euca2ools.commands.euca.describevolumes and euca2ools.commands.euca.describeinstances directly to avoid being affected by internal class/module changes on next major euca release. Instead block-storage-broker sets up a couple of simple parse_ec2* functions to help parsing the output of euca-describe-instances, euca-describe-volumes and euca-describe-tags commands. The euca response types supported by the parse_ec2* functions are INSTANCE, ATTACHMENT, VOLUME and TAG.

 2. To handle the fact that euca-2.X doesn't report volume TAG responses in the euca-describe-volumes command output, _ec2_describe_volumes also calls _ec2_describe_tags to augment any volume tag data (which we use to label volumes in EC2).

 3. In using euca-describe-tags to augment existing volume data I found that tags can be orphaned once a volume is removed by an admin via euca-delete-volume, if that is the case we will ignore tags for volumes that don't exist and log a message

 4. In unit tests drop all MockEuca* classes as they are no longer needed because we mock the subprocess.check_output results for the euca-* commands we run.

By parsing the output from the euca-* commands, we shield ourselves from the internal changes in class structure definition in future euca releases. This branch adds very basic validation of euca-describe-instance output. We can add more validation in separate branches as I didn't want this MP to grow too large.

To post a comment you must log in.
87. By Chad Smith

merge fixes from bsb-ec2-support and resolve conflicts + lint

88. By Chad Smith

revert Makefile change

89. By Chad Smith

Ignore any orphaned TAG lines from euca-describe-tags that don't map to current existing volume-ids

Revision history for this message
Chad Smith (chad.smith) wrote :

Here's a deployer bundle to deploy.
rm -rf ./trusty wherever you are about to juju-deployer from to ensure you don't use cached charm checkouts

cat block-storage-trusty.yaml

common:
    services:
        storage:
            branch: lp:~chad.smith/charms/precise/storage/storage-volume-label-availability-zone
            options:
                provider: block-storage-broker
                volume_size: 9
        block-storage-broker-ec2:
            branch: lp:~chad.smith/charms/precise/block-storage-broker/bsb-trusty-support
            options:
                provider: ec2
                key: <YOUR_EC2_ACCESS_KEY>
                endpoint: <YOUR_EC2_URL>
                secret: <YOUR_EC2_SECRET_KEY>
        postgresql:
            branch: lp:~chad.smith/charms/precise/postgresql/postgresql-using-storage-subordinate
            constraints: mem=2048
            options:
                extra-packages: python-apt postgresql-contrib postgresql-9.3-debversion
                max_connections: 500

doit:
    inherits: common
    series: trusty
    relations:
        - [postgresql, storage]
        - [storage, block-storage-broker-ec2]

juju-deployer -c block-storage-trusty.yaml doit

Revision history for this message
Chad Smith (chad.smith) wrote :

The bundle above works from the w/ juju 1.17.6 be wary of 1.17. < 6 as it suffers from bug 1285901

Revision history for this message
Alberto Donato (ack) wrote :

As discussed on IRC, I think the code could be refactored to reduce duplication, for example as in https://pastebin.canonical.com/107195/.
Tests could then mock _run_command rather than subprocess.check_output.

It should be even possible to perform the line.split("\n") in the _run_command method, returning a list of lists (so the parse_* methods don't need to perform it), I'm not sure if it fits all the use cases, though.

#1:
+ data["device"] = ""
+ data["volume_label"] = ""
+ data["instance_id"] = ""
+ data["tags"] = {"volume_name": ""}

These can be set with a single data.update()

#2:
+ volume_data.update({"instance_id": data["instance_id"]})
+ volume_data.update({"device": data["device"]})

Same as #1

#3:
+ for tag_volume_id in volume_tag_data.keys():
+ additional_tags = volume_tag_data[tag_volume_id]["tags"]
+ if tag_volume_id not in result:
+ hookenv.log(
+ "Ignoring tags for volume-id %s that doesn't exist" %
+ tag_volume_id)
             else:

The first line inside the loop can be moved inside the else clause.
Also there's an extra indentation space on the hookenv.log() call

#4:
I think you can drop the checks on the response_type in the parse_* methods, since it's already checked by call sites (related tests can be dropped then).

review: Needs Fixing
90. By Chad Smith

pull in ack's _run_command patch for simplification

Revision history for this message
Chad Smith (chad.smith) wrote :

Rejecting this branch in favor of dropping euca2ools and using Amazon's python SDK: python-boto as boto is more stable across Ubuntu releases than trying to parse and unify the output of euca2ools commandline utilities.

review: Disapprove

Unmerged revisions

90. By Chad Smith

pull in ack's _run_command patch for simplification

89. By Chad Smith

Ignore any orphaned TAG lines from euca-describe-tags that don't map to current existing volume-ids

88. By Chad Smith

revert Makefile change

87. By Chad Smith

merge fixes from bsb-ec2-support and resolve conflicts + lint

86. By Chad Smith

now that _ec2_describe_volumes calls euca-describe-tags, we need to mock that in unit tests. Add unit tests for parse_ec2_*_reponse functions

85. By Chad Smith

docstring updates

84. By Chad Smith

perform validation of INSTANCE reponse_type before attmpting to parse the value in the line. lint fixes in unit tests

83. By Chad Smith

parse euca-describe-instances and euca-describe-volumes output instead of tying into euca2ools.commands.euca.describeinstances directly

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'hooks/test_util.py'
--- hooks/test_util.py 2014-03-21 17:42:00 +0000
+++ hooks/test_util.py 2014-08-08 02:34:47 +0000
@@ -1,5 +1,9 @@
1import util1import util
2from util import StorageServiceUtil, ENVIRONMENT_MAP, generate_volume_label2from util import (
3 StorageServiceUtil, EUCA2_INSTANCE_RESPONSE_LENGTH_V3,
4 EUCA2_INSTANCE_RESPONSE_LENGTH_V2, parse_ec2_tag_response,
5 parse_ec2_volume_response, parse_ec2_instance_response,
6 parse_ec2_attachment_response, ENVIRONMENT_MAP, generate_volume_label)
3import mocker7import mocker
4import os8import os
5import subprocess9import subprocess
@@ -9,7 +13,6 @@
9class TestNovaUtil(mocker.MockerTestCase):13class TestNovaUtil(mocker.MockerTestCase):
1014
11 def setUp(self):15 def setUp(self):
12 super(TestNovaUtil, self).setUp()
13 self.maxDiff = None16 self.maxDiff = None
14 util.hookenv = TestHookenv(17 util.hookenv = TestHookenv(
15 {"key": "myusername", "tenant": "myusername_project",18 {"key": "myusername", "tenant": "myusername_project",
@@ -880,59 +883,9 @@
880 message, util.hookenv._log_INFO, "Not logged- %s" % message)883 message, util.hookenv._log_INFO, "Not logged- %s" % message)
881884
882885
883class MockEucaCommand(object):
884 def __init__(self, result):
885 self.result = result
886
887 def main(self):
888 return self.result
889
890
891class MockEucaReservation(object):
892 def __init__(self, instances):
893 self.instances = instances
894 self.id = 1
895
896
897class MockEucaInstance(object):
898 def __init__(self, instance_id=None, ip_address=None, image_id=None,
899 instance_type=None, kernel=None, private_dns_name=None,
900 public_dns_name=None, state=None, tags=[],
901 availability_zone=None):
902 self.id = instance_id
903 self.ip_address = ip_address
904 self.image_id = image_id
905 self.instance_type = instance_type
906 self.kernel = kernel
907 self.private_dns_name = private_dns_name
908 self.public_dns_name = public_dns_name
909 self.state = state
910 self.tags = tags
911 self.placement = availability_zone
912
913
914class MockAttachData(object):
915 def __init__(self, device, instance_id):
916 self.device = device
917 self.instance_id = instance_id
918
919
920class MockVolume(object):
921 def __init__(self, vol_id, device, instance_id, zone, size, status,
922 snapshot_id, tags):
923 self.id = vol_id
924 self.attach_data = MockAttachData(device, instance_id)
925 self.zone = zone
926 self.size = size
927 self.status = status
928 self.snapshot_id = snapshot_id
929 self.tags = tags
930
931
932class TestEC2Util(mocker.MockerTestCase):886class TestEC2Util(mocker.MockerTestCase):
933887
934 def setUp(self):888 def setUp(self):
935 super(TestEC2Util, self).setUp()
936 self.maxDiff = None889 self.maxDiff = None
937 util.hookenv = TestHookenv(890 util.hookenv = TestHookenv(
938 {"key": "ec2key", "secret": "ec2password",891 {"key": "ec2key", "secret": "ec2password",
@@ -1005,8 +958,8 @@
1005 configuration options.958 configuration options.
1006 """959 """
1007 command = "euca-describe-instances"960 command = "euca-describe-instances"
1008 nova_cmd = self.mocker.replace(subprocess.check_call)961 euca_cmd = self.mocker.replace(subprocess.check_call)
1009 nova_cmd(command, shell=True)962 euca_cmd(command, shell=True)
1010 self.mocker.replay()963 self.mocker.replay()
1011964
1012 self.storage.validate_credentials()965 self.storage.validate_credentials()
@@ -1242,39 +1195,47 @@
12421195
1243 def test_wb_ec2_describe_volumes_command_error(self):1196 def test_wb_ec2_describe_volumes_command_error(self):
1244 """1197 """
1245 L{_ec2_describe_volumes} will exit in error when the euca2ools1198 L{_ec2_describe_volumes} will exit in error when the
1246 C{DescribeVolumes} command fails.1199 C{euca-describe-volumes} command fails.
1247 """1200 """
1248 euca_command = self.mocker.replace(self.storage.ec2_volume_class)1201 command = "euca-describe-volumes"
1249 euca_command()1202 euca_volumes = self.mocker.replace(subprocess.check_output)
1250 self.mocker.throw(SystemExit(1))1203 euca_volumes(command, shell=True)
1204 self.mocker.throw(subprocess.CalledProcessError(1, command))
1251 self.mocker.replay()1205 self.mocker.replay()
12521206
1253 result = self.assertRaises(1207 result = self.assertRaises(
1254 SystemExit, self.storage._ec2_describe_volumes)1208 SystemExit, self.storage._ec2_describe_volumes)
1255 self.assertEqual(result.code, 1)1209 self.assertEqual(result.code, 1)
1256 message = "ERROR: Couldn't contact EC2 using euca-describe-volumes"1210 message = (
1211 "ERROR: Command '%s' returned non-zero exit status 1" % command)
1257 self.assertIn(1212 self.assertIn(
1258 message, util.hookenv._log_ERROR, "Not logged- %s" % message)1213 message, util.hookenv._log_ERROR, "Not logged- %s" % message)
12591214
1260 def test_wb_ec2_describe_volumes_without_attached_instances(self):1215 def test_wb_ec2_describe_volumes_without_attached_instances(self):
1261 """1216 """
1262 L{_ec2_describe_volumes} parses the results of euca2ools1217 L{_ec2_describe_volumes} parses the output from the
1263 C{DescribeVolumes} to create a C{dict} of volume information. When no1218 C{euca-describe-volumes} command and returns a C{dict} of volume
1264 C{instance_id}s are present the volumes are not attached so no1219 information. When no C{ATTACHMENT} response types are present the
1265 C{device} or C{instance_id} information will be present.1220 volumes are not attached so no C{device} or C{instance_id} information
1221 will be present. Any C{TAG} response types will be added to the
1222 associated volume.
1266 """1223 """
1267 volume1 = MockVolume(1224 volume_output = (
1268 "123-123-123", device="/dev/notshown", instance_id="notseen",1225 "VOLUME\t123-123-123\t 10\tsome-shot\tec2-az1\tavailable\t"
1269 zone="ec2-az1", size="10", status="available",1226 "2014-03-19T22:00:02.580Z\nVOLUME\t456-456-456\t 8\tsome-shot\t"
1270 snapshot_id="some-shot", tags={})1227 "ec2-az2\tavailable\t2014-03-19T22:00:02.580Z\n"
1271 volume2 = MockVolume(1228 "TAG\tvolume\t456-456-456\tvolume_name\tmy volume name")
1272 "456-456-456", device="/dev/notshown", instance_id="notseen",1229 volume_command = "euca-describe-volumes"
1273 zone="ec2-az2", size="8", status="available",1230 tag_output = (
1274 snapshot_id="some-shot", tags={"volume_name": "my volume name"})1231 "TAG\t456-456-456\tvolume\tother_key\tother value")
1275 euca_command = self.mocker.replace(self.storage.ec2_volume_class)1232 tag_command = "euca-describe-tags"
1276 euca_command()1233
1277 self.mocker.result(MockEucaCommand([volume1, volume2]))1234 euca_command = self.mocker.replace(subprocess.check_output)
1235 euca_command(volume_command, shell=True)
1236 self.mocker.result(volume_output)
1237 euca_command(tag_command, shell=True)
1238 self.mocker.result(tag_output)
1278 self.mocker.replay()1239 self.mocker.replay()
12791240
1280 expected = {"123-123-123": {"id": "123-123-123", "status": "available",1241 expected = {"123-123-123": {"id": "123-123-123", "status": "available",
@@ -1290,27 +1251,30 @@
1290 "volume_label": "my volume name",1251 "volume_label": "my volume name",
1291 "size": "8", "instance_id": "",1252 "size": "8", "instance_id": "",
1292 "snapshot_id": "some-shot",1253 "snapshot_id": "some-shot",
1293 "tags": {"volume_name": "my volume name"}}}1254 "tags": {"volume_name": "my volume name",
1255 "other_key": "other value"}}}
1294 self.assertEqual(self.storage._ec2_describe_volumes(), expected)1256 self.assertEqual(self.storage._ec2_describe_volumes(), expected)
12951257
1296 def test_wb_ec2_describe_volumes_matches_volume_id_supplied(self):1258 def test_wb_ec2_describe_volumes_matches_volume_id_supplied(self):
1297 """1259 """
1298 L{_ec2_describe_volumes} parses the results of euca2ools1260 L{_ec2_describe_volumes} parses the output of the
1299 C{DescribeVolumes} to create a C{dict} of volume information.1261 C{euca-describe-volumes} command and returns a C{dict} of volume
1300 When C{volume_id} is provided return a C{dict} for the matched volume.1262 information. When C{volume_id} is provided, return a C{dict} for the
1263 matched volume.
1301 """1264 """
1302 volume_id = "123-123-123"1265 volume_id = "123-123-123"
1303 volume1 = MockVolume(1266
1304 volume_id, device="/dev/notshown", instance_id="notseen",1267 output = (
1305 zone="ec2-az1", size="10", status="available",1268 "VOLUME\t123-123-123\t 10\tsome-shot\tec2-az1\tavailable\t"
1306 snapshot_id="some-shot", tags={})1269 "2014-03-19T22:00:02.580Z\nVOLUME\t456-456-456\t 8\tsome-shot\t"
1307 volume2 = MockVolume(1270 "ec2-az2\tavailable\t2014-03-19T22:00:02.580Z\n")
1308 "456-456-456", device="/dev/notshown", instance_id="notseen",1271 command = "euca-describe-volumes"
1309 zone="ec2-az2", size="8", status="available",1272
1310 snapshot_id="some-shot", tags={"volume_name": "my volume name"})1273 euca_command = self.mocker.replace(subprocess.check_output)
1311 euca_command = self.mocker.replace(self.storage.ec2_volume_class)1274 euca_command(command, shell=True)
1312 euca_command()1275 self.mocker.result(output)
1313 self.mocker.result(MockEucaCommand([volume1, volume2]))1276 euca_command("euca-describe-tags", shell=True)
1277 self.mocker.result("\n")
1314 self.mocker.replay()1278 self.mocker.replay()
13151279
1316 expected = {1280 expected = {
@@ -1323,18 +1287,22 @@
13231287
1324 def test_wb_ec2_describe_volumes_unmatched_volume_id_supplied(self):1288 def test_wb_ec2_describe_volumes_unmatched_volume_id_supplied(self):
1325 """1289 """
1326 L{_ec2_describe_volumes} parses the results of euca2ools1290 L{_ec2_describe_volumes} parses the output of the
1327 C{DescribeVolumes} to create a C{dict} of volume information.1291 C{euca-describe-volumes} command and returns a C{dict} of volume
1328 When C{volume_id} is provided and unmatched, return an empty C{dict}.1292 information. When C{volume_id} is provided and unmatched, return an
1293 empty C{dict}.
1329 """1294 """
1330 unmatched_volume_id = "456-456-456"1295 unmatched_volume_id = "456-456-456"
1331 volume1 = MockVolume(1296 output = (
1332 "123-123-123", device="/dev/notshown", instance_id="notseen",1297 "VOLUME\t123-123-123\t 10\tsome-shot\tec2-az1\tavailable\t"
1333 zone="ec2-az1", size="10", status="available",1298 "2014-03-19T22:00:02.580Z\n")
1334 snapshot_id="some-shot", tags={})1299 command = "euca-describe-volumes"
1335 euca_command = self.mocker.replace(self.storage.ec2_volume_class)1300
1336 euca_command()1301 euca_command = self.mocker.replace(subprocess.check_output)
1337 self.mocker.result(MockEucaCommand([volume1]))1302 euca_command(command, shell=True)
1303 self.mocker.result(output)
1304 euca_command("euca-describe-tags", shell=True)
1305 self.mocker.result("\n")
1338 self.mocker.replay()1306 self.mocker.replay()
13391307
1340 self.assertEqual(1308 self.assertEqual(
@@ -1342,22 +1310,28 @@
13421310
1343 def test_wb_ec2_describe_volumes_with_attached_instances(self):1311 def test_wb_ec2_describe_volumes_with_attached_instances(self):
1344 """1312 """
1345 L{_ec2_describe_volumes} parses the results of euca2ools1313 L{_ec2_describe_volumes} parses the output of the
1346 C{DescribeVolumes} to create a C{dict} of volume information. If1314 C{euca-describe-volumes} command and returns a C{dict} of volume
1347 C{status} is C{in-use}, both C{device} and C{instance_id} will be1315 information. When C{status} is C{in-use}, both C{device} and
1348 returned in the C{dict}.1316 C{instance_id} will be returned in the C{dict}.
1349 """1317 """
1350 volume1 = MockVolume(1318 output = (
1351 "123-123-123", device="/dev/notshown", instance_id="notseen",1319 "VOLUME\t123-123-123\t 10\tsome-shot\tec2-az1\tavailable\t"
1352 zone="ec2-az1", size="10", status="available",1320 "2014-03-19T22:00:02.580Z\nVOLUME\t456-456-456\t 8\tsome-shot\t"
1353 snapshot_id="some-shot", tags={})1321 "ec2-az2\tin-use\t2014-03-19T22:00:02.580Z\nATTACHMENT\t"
1354 volume2 = MockVolume(1322 "456-456-456\ti-456456\t/dev/xvdc\tattached\t"
1355 "456-456-456", device="/dev/xvdc", instance_id="i-456456",1323 "2014-03-19T22:00:02.000Z\nTAG\tvolume\t456-456-456\tvolume_name\t"
1356 zone="ec2-az2", size="8", status="in-use",1324 "my volume name\n")
1357 snapshot_id="some-shot", tags={"volume_name": "my volume name"})1325 command = "euca-describe-volumes"
1358 euca_command = self.mocker.replace(self.storage.ec2_volume_class)1326 tag_output = (
1359 euca_command()1327 "TAG\t456-456-456\tvolume\tvolume_name\tmy volume name")
1360 self.mocker.result(MockEucaCommand([volume1, volume2]))1328 tag_command = "euca-describe-tags"
1329
1330 euca_command = self.mocker.replace(subprocess.check_output)
1331 euca_command(command, shell=True)
1332 self.mocker.result(output)
1333 euca_command(tag_command, shell=True)
1334 self.mocker.result(tag_output)
1361 self.mocker.replay()1335 self.mocker.replay()
13621336
1363 expected = {"123-123-123": {"id": "123-123-123", "status": "available",1337 expected = {"123-123-123": {"id": "123-123-123", "status": "available",
@@ -1377,6 +1351,47 @@
1377 self.assertEqual(1351 self.assertEqual(
1378 self.storage._ec2_describe_volumes(), expected)1352 self.storage._ec2_describe_volumes(), expected)
13791353
1354 def test_wb_ec2_describe_volumes_orphaned_tags(self):
1355 """
1356 L{_ec2_describe_volumes} parses the output of the
1357 C{euca-describe-volumes} and augments that data with volume-related
1358 tags from C{euca-describe-tags}. When C{euca-describe-tags} returns
1359 information about orphaned tags for volumes that no longer exist,
1360 L{_ec2_describe_volumes} will log this information and move on.
1361 """
1362 output = (
1363 "VOLUME\t456-456-456\t 8\tsome-shot\t"
1364 "ec2-az2\tin-use\t2014-03-19T22:00:02.580Z\nATTACHMENT\t"
1365 "456-456-456\ti-456456\t/dev/xvdc\tattached\t"
1366 "2014-03-19T22:00:02.000Z\nTAG\tvolume\t456-456-456\tvolume_name\t"
1367 "my volume name\n")
1368 command = "euca-describe-volumes"
1369 tag_output = (
1370 "TAG\tvolume-not-here\tvolume\tvolume_name\tmy volume name\n"
1371 "TAG\t456-456-456\tvolume\tvolume_name\tmy volume name")
1372 tag_command = "euca-describe-tags"
1373
1374 euca_command = self.mocker.replace(subprocess.check_output)
1375 euca_command(command, shell=True)
1376 self.mocker.result(output)
1377 euca_command(tag_command, shell=True)
1378 self.mocker.result(tag_output)
1379 self.mocker.replay()
1380
1381 expected = {"456-456-456": {"id": "456-456-456", "status": "in-use",
1382 "device": "/dev/xvdc",
1383 "availability_zone": "ec2-az2",
1384 "volume_label": "my volume name",
1385 "size": "8", "instance_id": "i-456456",
1386 "snapshot_id": "some-shot",
1387 "tags": {"volume_name": "my volume name"}}}
1388 self.assertEqual(
1389 self.storage._ec2_describe_volumes(), expected)
1390 message = (
1391 "Ignoring tags for volume-id volume-not-here that doesn't exist")
1392 self.assertIn(
1393 message, util.hookenv._log_INFO, "Not logged- %s" % message)
1394
1380 def test_wb_ec2_create_volume(self):1395 def test_wb_ec2_create_volume(self):
1381 """1396 """
1382 L{_ec2_create_volume} uses the command C{euca-create-volume} to create1397 L{_ec2_create_volume} uses the command C{euca-create-volume} to create
@@ -1392,18 +1407,17 @@
1392 zone = "ec2-az3"1407 zone = "ec2-az3"
1393 command = "euca-create-volume -z %s -s %s" % (zone, size)1408 command = "euca-create-volume -z %s -s %s" % (zone, size)
13941409
1395 reservation = MockEucaReservation(
1396 [MockEucaInstance(
1397 instance_id=instance_id, availability_zone=zone)])
1398 euca_command = self.mocker.replace(self.storage.ec2_instance_class)
1399 euca_command()
1400 self.mocker.result(MockEucaCommand([reservation]))
1401 create = self.mocker.replace(subprocess.check_output)1410 create = self.mocker.replace(subprocess.check_output)
1402 create(command, shell=True)1411 create(command, shell=True)
1403 self.mocker.result(1412 self.mocker.result(
1404 "VOLUME %s 9 nova creating 2014-03-14T17:26:20\n" % volume_id)1413 "VOLUME %s 9 nova creating 2014-03-14T17:26:20\n" % volume_id)
1405 self.mocker.replay()1414 self.mocker.replay()
14061415
1416 def mock_describe_instances(my_id):
1417 self.assertEqual(my_id, instance_id)
1418 return {"availability_zone": zone}
1419 self.storage._ec2_describe_instances = mock_describe_instances
1420
1407 def mock_describe_volumes(my_id):1421 def mock_describe_volumes(my_id):
1408 self.assertEqual(my_id, volume_id)1422 self.assertEqual(my_id, volume_id)
1409 return {"id": volume_id}1423 return {"id": volume_id}
@@ -1436,10 +1450,10 @@
1436 volume_label = "postgresql/0 unit volume"1450 volume_label = "postgresql/0 unit volume"
1437 size = 101451 size = 10
14381452
1439 euca_command = self.mocker.replace(self.storage.ec2_instance_class)1453 def mock_describe_instances(my_id):
1440 euca_command()1454 self.assertEqual(my_id, instance_id)
1441 self.mocker.result(MockEucaCommand([])) # Empty results from euca1455 return {} # No results found
1442 self.mocker.replay()1456 self.storage._ec2_describe_instances = mock_describe_instances
14431457
1444 result = self.assertRaises(1458 result = self.assertRaises(
1445 SystemExit, self.storage._ec2_create_volume, size, volume_label,1459 SystemExit, self.storage._ec2_create_volume, size, volume_label,
@@ -1465,17 +1479,16 @@
1465 zone = "ec2-az3"1479 zone = "ec2-az3"
1466 command = "euca-create-volume -z %s -s %s" % (zone, size)1480 command = "euca-create-volume -z %s -s %s" % (zone, size)
14671481
1468 reservation = MockEucaReservation(
1469 [MockEucaInstance(
1470 instance_id=instance_id, availability_zone=zone)])
1471 euca_command = self.mocker.replace(self.storage.ec2_instance_class)
1472 euca_command()
1473 self.mocker.result(MockEucaCommand([reservation]))
1474 create = self.mocker.replace(subprocess.check_output)1482 create = self.mocker.replace(subprocess.check_output)
1475 create(command, shell=True)1483 create(command, shell=True)
1476 self.mocker.result("INSTANCE invalid-instance-type-response\n")1484 self.mocker.result("INSTANCE invalid-instance-type-response\n")
1477 self.mocker.replay()1485 self.mocker.replay()
14781486
1487 def mock_describe_instances(my_id):
1488 self.assertEqual(my_id, instance_id)
1489 return {"availability_zone": zone}
1490 self.storage._ec2_describe_instances = mock_describe_instances
1491
1479 def mock_describe_volumes(my_id):1492 def mock_describe_volumes(my_id):
1480 raise Exception("_ec2_describe_volumes should not be called")1493 raise Exception("_ec2_describe_volumes should not be called")
1481 self.storage._ec2_describe_volumes = mock_describe_volumes1494 self.storage._ec2_describe_volumes = mock_describe_volumes
@@ -1507,18 +1520,17 @@
1507 zone = "ec2-az3"1520 zone = "ec2-az3"
1508 command = "euca-create-volume -z %s -s %s" % (zone, size)1521 command = "euca-create-volume -z %s -s %s" % (zone, size)
15091522
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)1523 create = self.mocker.replace(subprocess.check_output)
1517 create(command, shell=True)1524 create(command, shell=True)
1518 self.mocker.result(1525 self.mocker.result(
1519 "VOLUME %s 9 nova creating 2014-03-14T17:26:20\n" % volume_id)1526 "VOLUME %s 9 nova creating 2014-03-14T17:26:20\n" % volume_id)
1520 self.mocker.replay()1527 self.mocker.replay()
15211528
1529 def mock_describe_instances(my_id):
1530 self.assertEqual(my_id, instance_id)
1531 return {"availability_zone": zone}
1532 self.storage._ec2_describe_instances = mock_describe_instances
1533
1522 def mock_describe_volumes(my_id):1534 def mock_describe_volumes(my_id):
1523 self.assertEqual(my_id, volume_id)1535 self.assertEqual(my_id, volume_id)
1524 return {} # No details found for this volume1536 return {} # No details found for this volume
@@ -1548,17 +1560,16 @@
1548 zone = "ec2-az3"1560 zone = "ec2-az3"
1549 command = "euca-create-volume -z %s -s %s" % (zone, size)1561 command = "euca-create-volume -z %s -s %s" % (zone, size)
15501562
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)1563 create = self.mocker.replace(subprocess.check_output)
1558 create(command, shell=True)1564 create(command, shell=True)
1559 self.mocker.throw(subprocess.CalledProcessError(1, command))1565 self.mocker.throw(subprocess.CalledProcessError(1, command))
1560 self.mocker.replay()1566 self.mocker.replay()
15611567
1568 def mock_describe_instances(my_id):
1569 self.assertEqual(my_id, instance_id)
1570 return {"availability_zone": zone}
1571 self.storage._ec2_describe_instances = mock_describe_instances
1572
1562 def mock_exception(my_id):1573 def mock_exception(my_id):
1563 raise Exception("These methods should not be called")1574 raise Exception("These methods should not be called")
1564 self.storage._ec2_describe_volumes = mock_exception1575 self.storage._ec2_describe_volumes = mock_exception
@@ -1728,3 +1739,243 @@
1728 (volume_id, instance_id))1739 (volume_id, instance_id))
1729 self.assertIn(1740 self.assertIn(
1730 message, util.hookenv._log_INFO, "Not logged- %s" % message)1741 message, util.hookenv._log_INFO, "Not logged- %s" % message)
1742
1743 def test_wb_ec2_describe_instances_euca_v2(self):
1744 """
1745 L{_ec2_describe_instances} parses the output of the command
1746 C{euca-describe-instances} for euca2ools version 2.X and returns a
1747 C{dict} of instance information.
1748 """
1749 command = "euca-describe-instances"
1750 output = (
1751 "INSTANCE\ti-aaa\tami-aaa\taaa.amazonaws.com\t"
1752 "aaa.ec2.internal\trunning\t\t0\t\tm1.small\t"
1753 "2014-03-19T17:52:02.000Z\tus-east-1a\taki-aaa\t\t\t"
1754 "monitoring-disabled\t54.81.8.9\t10.138.61.2\t\t\tebs\t\t\t\t\t"
1755 "paravirtual\t\t\t\t")
1756
1757 ec2_list = self.mocker.replace(subprocess.check_output)
1758 ec2_list(command, shell=True)
1759 self.mocker.result(output)
1760 self.mocker.replay()
1761
1762 expected = {
1763 "i-aaa": {
1764 "ip-address": "54.81.8.9", "private-ip-address": "10.138.61.2",
1765 "kernel": "aki-aaa", "instance-type": "m1.small",
1766 "state": "running", "public-dns-name": "aaa.amazonaws.com",
1767 "private-dns-name": "aaa.ec2.internal", "instance_id": "i-aaa",
1768 "image-id": "ami-aaa", "availability_zone": "us-east-1a"}}
1769
1770 self.assertEqual(
1771 EUCA2_INSTANCE_RESPONSE_LENGTH_V2, len(output.split("\t")))
1772
1773 self.assertEqual(
1774 self.storage._ec2_describe_instances(), expected)
1775
1776 def test_wb_ec2_describe_instances_euca_invalid_output_version(self):
1777 """
1778 L{_ec2_describe_instances} parses the output of the command
1779 C{euca-describe-instances} and if it sees an output length that is
1780 unsupported, an error is logged.
1781 """
1782 command = "euca-describe-instances"
1783 output = (
1784 "INSTANCE\ti-aaa\tami-0b9c9f62\tblah.amazonaws.com\t"
1785 "blah.ec2.internal\trunning\t\t0\t")
1786
1787 ec2_list = self.mocker.replace(subprocess.check_output)
1788 ec2_list(command, shell=True)
1789 self.mocker.result(output)
1790 self.mocker.replay()
1791
1792 self.assertNotIn(
1793 len(output.split("\t")),
1794 [EUCA2_INSTANCE_RESPONSE_LENGTH_V3,
1795 EUCA2_INSTANCE_RESPONSE_LENGTH_V3])
1796
1797 result = self.assertRaises(
1798 SystemExit, self.storage._ec2_describe_instances)
1799 self.assertEqual(result.code, 1)
1800 message = "ERROR: Unsupported euca INSTANCE output format version"
1801 self.assertIn(
1802 message, util.hookenv._log_ERROR, "Not logged- %s" % message)
1803
1804 def test_wb_ec2_describe_instances_specific_instance_id(self):
1805 """
1806 L{_ec2_describe_instances} parses the output of the command
1807 C{euca-describe-instances} for euca2ools version 3.X and returns a
1808 C{dict} of instance information. When C{instance_id} is provided,
1809 results are filtered to only the matching C{instance_id}.
1810 """
1811 command = "euca-describe-instances"
1812 output = (
1813 "INSTANCE\ti-aaa\tami-aaa\taaa.amazonaws.com\t"
1814 "aaa.ec2.internal\trunning\t\t0\t\tm1.small\t"
1815 "2014-03-19T17:52:02.000Z\tus-east-1a\taki-aaa\t\t\t"
1816 "monitoring-disabled\t54.81.8.1\t10.138.61.1\t\t\tebs"
1817 "\t\t\t\t\tparavirtual\txen\t"
1818 "5926eb12c796888691d9fa135f6e6f8ada297a9b1ecbea0c12282522400636f3"
1819 "\tsg-55e2e33e,sg-29e2e342\tdefault\tfalse\t\n"
1820 "INSTANCE\ti-bbb\tami-bbb\tbbb.amazonaws.com\t"
1821 "bbb.ec2.internal\trunning\t\t0\t\tm1.small\t"
1822 "2014-03-19T17:52:02.000Z\tus-east-1a\taki-bbb\t\t\t"
1823 "monitoring-disabled\t54.81.8.2\t10.138.61.2\t\t\tebs"
1824 "\t\t\t\t\tparavirtual\txen\t"
1825 "5926eb12c796888691d9fa135f6e6f8ada297a9b1ecbea0c12282522400636f3"
1826 "\tsg-55e2e33e,sg-29e2e342\tdefault\tfalse\t")
1827
1828 ec2_list = self.mocker.replace(subprocess.check_output)
1829 ec2_list(command, shell=True)
1830 self.mocker.result(output)
1831 self.mocker.replay()
1832
1833 expected = {
1834 "ip-address": "54.81.8.1", "private-ip-address": "10.138.61.1",
1835 "kernel": "aki-aaa", "instance-type": "m1.small",
1836 "state": "running", "public-dns-name": "aaa.amazonaws.com",
1837 "private-dns-name": "aaa.ec2.internal", "instance_id": "i-aaa",
1838 "image-id": "ami-aaa", "availability_zone": "us-east-1a"}
1839
1840 self.assertEqual(
1841 EUCA2_INSTANCE_RESPONSE_LENGTH_V3,
1842 len(output.split("\n")[0].split("\t")))
1843
1844 self.assertEqual(
1845 self.storage._ec2_describe_instances("i-aaa"), expected)
1846
1847 def test_parse_ec2_tag_response_not_tag_response(self):
1848 """
1849 L{parse_ec2_tag_response} returns an empty C{dict} when the
1850 line does not represent a C{TAG} response type.
1851 """
1852 non_tag_output_lines = (
1853 "VOLUME\t123-123-123\t 10\tsome-shot\tec2-az1\tavailable\t"
1854 "2014-03-19T22:00:02.580Z\n"
1855 "ATTACHMENT\t123-123-123\ti-123123\t/dev/xvdc\tattached\t"
1856 "2014-03-19T22:00:02.000Z\n"
1857 "INSTANCE\ti-aaa\tami-aaa\taaa.amazonaws.com\t"
1858 "aaa.ec2.internal\trunning\t\t0\t\tm1.small\t"
1859 "2014-03-19T17:52:02.000Z\tus-east-1a\taki-aaa\t\t\t"
1860 "monitoring-disabled\t54.81.8.1\t10.138.61.1\t\t\tebs"
1861 "\t\t\t\t\tparavirtual\txen\t"
1862 "5926eb12c796888691d9fa135f6e6f8ada297a9b1ecbea0c12282522400636f3"
1863 "\tsg-55e2e33e,sg-29e2e342\tdefault\tfalse\t")
1864 for line in non_tag_output_lines.split("\n"):
1865 self.assertEqual(parse_ec2_tag_response(line), {})
1866
1867 def test_parse_ec2_attachment_response_not_attachment_response(self):
1868 """
1869 L{parse_ec2_attachment_response} returns an empty C{dict} when the
1870 line does not represent a C{ATTACHMENT} response type.
1871 """
1872 non_attachment_output_lines = (
1873 "TAG\tvolume\t456-456-456\tvolume_name\tmy volume name\n"
1874 "VOLUME\t123-123-123\t 10\tsome-shot\tec2-az1\tavailable\t"
1875 "2014-03-19T22:00:02.580Z\n"
1876 "INSTANCE\ti-aaa\tami-aaa\taaa.amazonaws.com\t"
1877 "aaa.ec2.internal\trunning\t\t0\t\tm1.small\t"
1878 "2014-03-19T17:52:02.000Z\tus-east-1a\taki-aaa\t\t\t"
1879 "monitoring-disabled\t54.81.8.1\t10.138.61.1\t\t\tebs"
1880 "\t\t\t\t\tparavirtual\txen\t"
1881 "5926eb12c796888691d9fa135f6e6f8ada297a9b1ecbea0c12282522400636f3"
1882 "\tsg-55e2e33e,sg-29e2e342\tdefault\tfalse\t")
1883 for line in non_attachment_output_lines.split("\n"):
1884 self.assertEqual(parse_ec2_attachment_response(line), {})
1885
1886 def test_parse_ec2_volume_response_not_volume_response(self):
1887 """
1888 L{parse_ec2_volume_response} returns an empty C{dict} when the
1889 line does not represent a C{VOLUME} response type.
1890 """
1891 non_volume_output_lines = (
1892 "TAG\tvolume\t456-456-456\tvolume_name\tmy volume name\n"
1893 "ATTACHMENT\t123-123-123\ti-123123\t/dev/xvdc\tattached\t"
1894 "2014-03-19T22:00:02.000Z\n"
1895 "INSTANCE\ti-aaa\tami-aaa\taaa.amazonaws.com\t"
1896 "aaa.ec2.internal\trunning\t\t0\t\tm1.small\t"
1897 "2014-03-19T17:52:02.000Z\tus-east-1a\taki-aaa\t\t\t"
1898 "monitoring-disabled\t54.81.8.1\t10.138.61.1\t\t\tebs"
1899 "\t\t\t\t\tparavirtual\txen\t"
1900 "5926eb12c796888691d9fa135f6e6f8ada297a9b1ecbea0c12282522400636f3"
1901 "\tsg-55e2e33e,sg-29e2e342\tdefault\tfalse\t")
1902 for line in non_volume_output_lines.split("\n"):
1903 self.assertEqual(parse_ec2_volume_response(line), {})
1904
1905 def test_parse_ec2_instance_response_not_instance_response(self):
1906 """
1907 L{parse_ec2_instance_response} returns an empty C{dict} when the
1908 line does not represent a C{INSTANCE} response type.
1909 """
1910 non_instance_output_lines = (
1911 "TAG\tvolume\t456-456-456\tvolume_name\tmy volume name\n"
1912 "VOLUME\t123-123-123\t 10\tsome-shot\tec2-az1\tavailable\t"
1913 "2014-03-19T22:00:02.580Z\n"
1914 "ATTACHMENT\t123-123-123\ti-123123\t/dev/xvdc\tattached\t"
1915 "2014-03-19T22:00:02.000Z\n")
1916 for line in non_instance_output_lines.split("\n"):
1917 self.assertEqual(parse_ec2_instance_response(line), {})
1918
1919 def test_parse_ec2_instance_response(self):
1920 """
1921 L{parse_ec2_instance_response} returns a C{dict} of the parsed
1922 C{INSTANCE} response type.
1923 """
1924 output = (
1925 "INSTANCE\ti-aaa\tami-aaa\taaa.amazonaws.com\t"
1926 "aaa.ec2.internal\trunning\t\t0\t\tm1.small\t"
1927 "2014-03-19T17:52:02.000Z\tus-east-1a\taki-aaa\t\t\t"
1928 "monitoring-disabled\t54.81.8.1\t10.138.61.1\t\t\tebs"
1929 "\t\t\t\t\tparavirtual\txen\t"
1930 "5926eb12c796888691d9fa135f6e6f8ada297a9b1ecbea0c12282522400636f3"
1931 "\tsg-55e2e33e,sg-29e2e342\tdefault\tfalse\t")
1932
1933 expected = {
1934 "ip-address": "54.81.8.1", "private-ip-address": "10.138.61.1",
1935 "image-id": "ami-aaa", "instance-type": "m1.small",
1936 "kernel": "aki-aaa", "private-dns-name": "aaa.ec2.internal",
1937 "public-dns-name": "aaa.amazonaws.com", "state": "running",
1938 "availability_zone": "us-east-1a", "instance_id": "i-aaa"}
1939
1940 self.assertEqual(parse_ec2_instance_response(output), expected)
1941
1942 def test_parse_ec2_tag_response(self):
1943 """
1944 L{parse_ec2_tag_response} returns a C{dict} of the parsed C{TAG}
1945 response type.
1946 """
1947 output = "TAG\tvolume\t456-456-456\tvolume_name\tmy volume name"
1948
1949 expected = {
1950 "tag_type": "volume", "id": "456-456-456", "key": "volume_name",
1951 "value": "my volume name"}
1952 self.assertEqual(parse_ec2_tag_response(output), expected)
1953
1954 def test_parse_ec2_attachment_response(self):
1955 """
1956 L{parse_ec2_attachment_response} returns a C{dict} of the parsed
1957 C{ATTACHMENT} response type.
1958 """
1959 output = (
1960 "ATTACHMENT\t123-123-123\ti-123123\t/dev/xvdc\tattached\t"
1961 "2014-03-19T22:00:02.000Z\n")
1962
1963 expected = {
1964 "volume_id": "123-123-123", "instance_id": "i-123123",
1965 "device": "/dev/xvdc", "attach_status": "attached"}
1966 self.assertEqual(parse_ec2_attachment_response(output), expected)
1967
1968 def test_parse_ec2_volume_response(self):
1969 """
1970 L{parse_ec2_volume_response} returns a C{dict} of the parsed
1971 C{VOLUME} response type.
1972 """
1973 output = (
1974 "VOLUME\t123-123-123\t 10\tsome-shot\tec2-az1\tavailable\t"
1975 "2014-03-19T22:00:02.580Z\n")
1976
1977 expected = {
1978 "id": "123-123-123", "size": "10",
1979 "snapshot_id": "some-shot", "availability_zone": "ec2-az1",
1980 "status": "available"}
1981 self.assertEqual(parse_ec2_volume_response(output), expected)
17311982
=== modified file 'hooks/util.py'
--- hooks/util.py 2014-03-21 17:39:48 +0000
+++ hooks/util.py 2014-08-08 02:34:47 +0000
@@ -23,6 +23,9 @@
23 "nova": {"validate": "nova list",23 "nova": {"validate": "nova list",
24 "detach": "nova volume-detach %s %s"}}24 "detach": "nova volume-detach %s %s"}}
2525
26EUCA2_INSTANCE_RESPONSE_LENGTH_V2 = 30
27EUCA2_INSTANCE_RESPONSE_LENGTH_V3 = 32
28
2629
27class StorageServiceUtil(object):30class StorageServiceUtil(object):
28 """Interact with an underlying cloud storage provider.31 """Interact with an underlying cloud storage provider.
@@ -45,11 +48,6 @@
45 self.environment_map = ENVIRONMENT_MAP[provider]48 self.environment_map = ENVIRONMENT_MAP[provider]
46 self.commands = PROVIDER_COMMANDS[provider]49 self.commands = PROVIDER_COMMANDS[provider]
47 self.required_config_options = REQUIRED_CONFIG_OPTIONS[provider]50 self.required_config_options = REQUIRED_CONFIG_OPTIONS[provider]
48 if provider == "ec2":
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
5351
54 def load_environment(self):52 def load_environment(self):
55 """53 """
@@ -212,83 +210,112 @@
212 return210 return
213211
214 # EC2-specific methods212 # EC2-specific methods
213 def _run_command(self, command):
214 try:
215 output = subprocess.check_call(command, shell=True)
216 except subprocess.CalledProcessError, e:
217 hookenv.log("ERROR: %s" % str(e), hookenv.ERROR)
218 sys.exit(1)
219
220 # Split the output by lines
221 lines = []
222 if output:
223 for line in output.split("\n"):
224 line = line.strip()
225 if line:
226 lines.append(line)
227 return lines
228
215 def _ec2_create_tag(self, volume_id, tag_name, tag_value=None):229 def _ec2_create_tag(self, volume_id, tag_name, tag_value=None):
216 """Attach a tag and optional C{tag_value} to the given C{volume_id}"""230 """Attach a tag and optional C{tag_value} to the given C{volume_id}"""
217 tag_string = tag_name231 tag_string = tag_name
218 if tag_value:232 if tag_value:
219 tag_string += "=%s" % tag_value233 tag_string += "=%s" % tag_value
220 command = 'euca-create-tags %s --tag "%s"' % (volume_id, tag_string)234 command = "euca-create-tags %s --tag \"%s\"" % (volume_id, tag_string)
221235
222 try:236 self._run_command(command)
223 subprocess.check_call(command, shell=True)
224 except subprocess.CalledProcessError, e:
225 hookenv.log(
226 "ERROR: Couldn't add tags to the resource. %s" % str(e),
227 hookenv.ERROR)
228 sys.exit(1)
229 hookenv.log("Tagged (%s) to %s." % (tag_string, volume_id))237 hookenv.log("Tagged (%s) to %s." % (tag_string, volume_id))
230238
231 def _ec2_describe_instances(self, instance_id=None):239 def _ec2_describe_instances(self, instance_id=None):
232 """240 """
233 Use euca2ools libraries to describe instances and return a C{dict}241 Use euca2ools command output to describe instances and return a C{dict}
234 """242 """
235 result = {}243 result = {}
236 try:244
237 command = self.ec2_instance_class()245 lines = self._run_command("euca-describe-instances")
238 reservations = command.main()246 for line in lines:
239 except SystemExit:247 response_type = line.split("\t")[0]
240 hookenv.log(248 if response_type != "INSTANCE":
241 "ERROR: Couldn't contact EC2 using euca-describe-instances",249 continue
242 hookenv.ERROR)250 instance_data = parse_ec2_instance_response(line)
243 sys.exit(1)251 result[instance_data["instance_id"]] = instance_data
244 for reservation in reservations:252
245 for inst in reservation.instances:
246 result[inst.id] = {
247 "ip-address": inst.ip_address, "image-id": inst.image_id,
248 "instance-type": inst.image_id, "kernel": inst.kernel,
249 "private-dns-name": inst.private_dns_name,
250 "public-dns-name": inst.public_dns_name,
251 "reservation-id": reservation.id,
252 "state": inst.state, "tags": inst.tags,
253 "availability_zone": inst.placement}
254 if instance_id:253 if instance_id:
255 if instance_id in result:254 if instance_id in result:
256 return result[instance_id]255 return result[instance_id]
257 return {}256 return {}
258 return result257 return result
259258
259 def _ec2_describe_volume_tags(self):
260 """
261 Parse output of C{euca-describe-tags} returning a C{dict} of volume ids
262 and any associated tags.
263 """
264 result = {}
265
266 lines = self._run_command("euca-describe-tags")
267 for line in lines:
268 response_type = line.split("\t")[0]
269 if response_type != "TAG":
270 continue
271 data = parse_ec2_tag_response(line)
272 if not result.get(data["id"]):
273 result[data["id"]] = {"tags": {}}
274 result[data["id"]]["tags"].update({data["key"]: data["value"]})
275 return result
276
260 def _ec2_describe_volumes(self, volume_id=None):277 def _ec2_describe_volumes(self, volume_id=None):
261 """278 """
262 Use euca2ools libraries to describe volumes and return a C{dict}279 Parse output of C{euca-describe-volumes} returning a C{dict} of volume
280 ids all related volume information.
263 """281 """
264 result = {}282 result = {}
265 try:283
266 command = self.ec2_volume_class()284 lines = self._run_command("euca-describe-volumes")
267 volumes = command.main()285 for line in lines:
268 except SystemExit:286 response_type = line.split("\t")[0]
269 hookenv.log(287 if response_type == "VOLUME":
270 "ERROR: Couldn't contact EC2 using euca-describe-volumes",288 data = parse_ec2_volume_response(line)
271 hookenv.ERROR)289 data["device"] = ""
272 sys.exit(1)290 data["volume_label"] = ""
273 for volume in volumes:291 data["instance_id"] = ""
274 result[volume.id] = {292 data["tags"] = {"volume_name": ""}
275 "device": "",293 result[data["id"]] = data
276 "instance_id": "",294 elif response_type == "TAG":
277 "size": volume.size,295 data = parse_ec2_tag_response(line)
278 "snapshot_id": volume.snapshot_id,296 tags = result[data["id"]]["tags"]
279 "status": volume.status,297 tags.update({data["key"]: data["value"]})
280 "tags": volume.tags,298 volume_name = tags.get("volume_name")
281 "id": volume.id,299 if volume_name:
282 "availability_zone": volume.zone}300 result[data["id"]]["volume_label"] = volume_name
283 if "volume_name" in volume.tags:301 elif response_type == "ATTACHMENT":
284 result[volume.id]["volume_label"] = volume.tags["volume_name"]302 data = parse_ec2_attachment_response(line)
285 else:303 volume_data = result[data["volume_id"]]
286 result[volume.id]["tags"]["volume_name"] = ""304 volume_data.update({"instance_id": data["instance_id"]})
287 result[volume.id]["volume_label"] = ""305 volume_data.update({"device": data["device"]})
288 if volume.status == "in-use":306 else:
289 result[volume.id]["instance_id"] = (307 continue
290 volume.attach_data.instance_id)308
291 result[volume.id]["device"] = volume.attach_data.device309 volume_tag_data = self._ec2_describe_volume_tags()
310 for tag_volume_id in volume_tag_data.keys():
311 additional_tags = volume_tag_data[tag_volume_id]["tags"]
312 if tag_volume_id not in result:
313 hookenv.log(
314 "Ignoring tags for volume-id %s that doesn't exist" %
315 tag_volume_id)
316 else:
317 result[tag_volume_id]["tags"].update(additional_tags)
318
292 if volume_id:319 if volume_id:
293 if volume_id in result:320 if volume_id in result:
294 return result[volume_id]321 return result[volume_id]
@@ -311,19 +338,15 @@
311 (instance_id, config_data["endpoint"]), hookenv.ERROR)338 (instance_id, config_data["endpoint"]), hookenv.ERROR)
312 sys.exit(1)339 sys.exit(1)
313340
314 try:341 command = "euca-create-volume -z %s -s %s" % (
315 output = subprocess.check_output(342 instance["availability_zone"], size)
316 "euca-create-volume -z %s -s %s" %
317 (instance["availability_zone"], size), shell=True)
318 except subprocess.CalledProcessError, e:
319 hookenv.log("ERROR: %s" % str(e), hookenv.ERROR)
320 sys.exit(1)
321343
322 response_type, volume_id = output.split()[:2]344 lines = self._run_command(command)
345 response_type, volume_id = lines[:2]
323 if response_type != "VOLUME":346 if response_type != "VOLUME":
324 hookenv.log(347 hookenv.log(
325 "ERROR: Didn't get VOLUME response from euca-create-volume. "348 "ERROR: Didn't get VOLUME response from euca-create-volume. "
326 "Response: %s" % output, hookenv.ERROR)349 "Response: %s" % "".join(lines), hookenv.ERROR)
327 sys.exit(1)350 sys.exit(1)
328 volume = self.describe_volumes(volume_id.strip())351 volume = self.describe_volumes(volume_id.strip())
329 if not volume:352 if not volume:
@@ -341,13 +364,10 @@
341 the device path.364 the device path.
342 """365 """
343 device = "/dev/xvdc"366 device = "/dev/xvdc"
344 try:367 command = "euca-attach-volume -i %s -d %s %s" % (
345 subprocess.check_call(368 instance_id, device, volume_id)
346 "euca-attach-volume -i %s -d %s %s" %369
347 (instance_id, device, volume_id), shell=True)370 self._run_command(command)
348 except subprocess.CalledProcessError, e:
349 hookenv.log("ERROR: %s" % str(e), hookenv.ERROR)
350 sys.exit(1)
351 return device371 return device
352372
353 # Nova-specific methods373 # Nova-specific methods
@@ -358,17 +378,9 @@
358 """378 """
359 from ast import literal_eval379 from ast import literal_eval
360 result = {"tags": {}, "instance_id": "", "device": ""}380 result = {"tags": {}, "instance_id": "", "device": ""}
361 command = "nova volume-show '%s'" % volume_id381
362 try:382 lines = self._run_command("nova volume-show '%s'" % volume_id)
363 output = subprocess.check_output(command, shell=True)383 for line in lines:
364 except subprocess.CalledProcessError, e:
365 hookenv.log(
366 "ERROR: Failed to get nova volume info. %s" % str(e),
367 hookenv.ERROR)
368 sys.exit(1)
369 for line in output.split("\n"):
370 if not line.strip(): # Skip empty lines
371 continue
372 if "+----" in line or "Property" in line:384 if "+----" in line or "Property" in line:
373 continue385 continue
374 (_, key, value, _) = line.split("|")386 (_, key, value, _) = line.split("|")
@@ -393,15 +405,9 @@
393 def _nova_describe_volumes(self, volume_id=None):405 def _nova_describe_volumes(self, volume_id=None):
394 """Create a C{dict} describing all nova volumes"""406 """Create a C{dict} describing all nova volumes"""
395 result = {}407 result = {}
396 command = "nova volume-list"408
397 try:409 lines = self._run_command("nova volume-list")
398 output = subprocess.check_output(command, shell=True)410 for line in lines:
399 except subprocess.CalledProcessError, e:
400 hookenv.log("ERROR: %s" % str(e), hookenv.ERROR)
401 sys.exit(1)
402 for line in output.split("\n"):
403 if not line.strip(): # Skip empty lines
404 continue
405 if "+----" in line or "ID" in line:411 if "+----" in line or "ID" in line:
406 continue412 continue
407 values = line.split("|")413 values = line.split("|")
@@ -434,15 +440,12 @@
434 Attach a Nova C{volume_id} to the provided C{instance_id} and return440 Attach a Nova C{volume_id} to the provided C{instance_id} and return
435 the device path.441 the device path.
436 """442 """
437 try:443 command = (
438 device = subprocess.check_output(444 "nova volume-attach %s %s auto | egrep -o \"/dev/vd[b-z]\"" %
439 "nova volume-attach %s %s auto | egrep -o \"/dev/vd[b-z]\"" %445 (instance_id, volume_id))
440 (instance_id, volume_id), shell=True)446 lines = self._run_ec2_command(command)
441 except subprocess.CalledProcessError, e:447 if lines:
442 hookenv.log("ERROR: %s" % str(e), hookenv.ERROR)448 return lines[0]
443 sys.exit(1)
444 if device.strip():
445 return device.strip()
446 return ""449 return ""
447450
448 def _nova_create_volume(self, size, volume_label, instance_id):451 def _nova_create_volume(self, size, volume_label, instance_id):
@@ -450,14 +453,11 @@
450 hookenv.log(453 hookenv.log(
451 "Creating a %sGig volume named (%s) for instance %s" %454 "Creating a %sGig volume named (%s) for instance %s" %
452 (size, volume_label, instance_id))455 (size, volume_label, instance_id))
453 try:456 command = (
454 subprocess.check_call(457 "nova volume-create --display-name '%s' %s" %
455 "nova volume-create --display-name '%s' %s" %458 (volume_label, size))
456 (volume_label, size), shell=True)
457 except subprocess.CalledProcessError, e:
458 hookenv.log("ERROR: %s" % str(e), hookenv.ERROR)
459 sys.exit(1)
460459
460 self._run_command(command)
461 volume_id = self.get_volume_id(volume_label)461 volume_id = self.get_volume_id(volume_label)
462 if not volume_id:462 if not volume_id:
463 hookenv.log(463 hookenv.log(
@@ -467,6 +467,102 @@
467 return volume_id467 return volume_id
468468
469469
470def parse_ec2_tag_response(line):
471 """
472 Parses a line of output of response type TAG from euca commands and
473 returns a C{dict} of the values. If not a TAG response, an empty C{dict} is
474 returned.
475 """
476 values = line.split("\t")
477 response_type = values[0]
478 if response_type != "TAG":
479 return {}
480
481 # There are two versions of the TAG response
482 if values[1] == "volume":
483 tag_type = values[1]
484 object_id = values[2]
485 else:
486 tag_type = values[2]
487 object_id = values[1]
488 if tag_type != "volume":
489 hookenv.log(
490 "Ignoring non-volume TAG response: %s." % line, hookenv.WARNING)
491 return {}
492 return {
493 "tag_type": tag_type, "id": object_id, "key": values[3],
494 "value": values[4]}
495
496
497def parse_ec2_attachment_response(line):
498 """
499 Parses a line of output of response type ATTACHMENT from euca commands and
500 returns a C{dict} of the values. If not a ATTACHMENT response, an empty
501 C{dict} is returned.
502 """
503 values = line.split("\t")
504 response_type = values[0]
505 if response_type != "ATTACHMENT":
506 return {}
507
508 return {
509 "volume_id": values[1], "instance_id": values[2], "device": values[3],
510 "attach_status": values[4]}
511
512
513def parse_ec2_volume_response(line):
514 """
515 Parses a line of output of response type VOLUME from euca commands and
516 returns a C{dict} of the values. If not a VOLUME response, an empty C{dict}
517 is returned.
518 """
519 values = line.split("\t")
520 response_type = values[0]
521 if response_type != "VOLUME":
522 return {}
523
524 return {
525 "id": values[1], "size": values[2].strip(),
526 "snapshot_id": values[3], "availability_zone": values[4],
527 "status": values[5]}
528
529
530def parse_ec2_instance_response(line):
531 """
532 Parses a line of output of response type INSTANCE from euca commands and
533 returns a C{dict} of the values. If not an INSTANCE response, and empty
534 C{dict} is returned.
535 """
536 values = line.split("\t")
537 response_type = values[0]
538 if response_type != "INSTANCE":
539 return {}
540
541 if len(values) not in [EUCA2_INSTANCE_RESPONSE_LENGTH_V2,
542 EUCA2_INSTANCE_RESPONSE_LENGTH_V3]:
543 hookenv.log(
544 "ERROR: Unsupported euca INSTANCE output format version",
545 hookenv.ERROR)
546 sys.exit(1)
547
548 (ec2_instance_id, image_id) = values[1:3]
549 public_dns = values[3]
550 private_dns = values[4]
551 state = values[5]
552 instance_type = values[9]
553 availability_zone = values[11]
554 kernel = values[12]
555 public_ip = values[16]
556 private_ip = values[17]
557
558 return {
559 "ip-address": public_ip, "private-ip-address": private_ip,
560 "image-id": image_id, "instance-type": instance_type,
561 "kernel": kernel, "private-dns-name": private_dns,
562 "public-dns-name": public_dns, "state": state,
563 "availability_zone": availability_zone, "instance_id": ec2_instance_id}
564
565
470def generate_volume_label(remote_unit):566def generate_volume_label(remote_unit):
471 """Create a volume label for the requesting remote unit"""567 """Create a volume label for the requesting remote unit"""
472 return "%s unit volume" % remote_unit568 return "%s unit volume" % remote_unit

Subscribers

People subscribed via source and target branches

to all changes: