Merge lp:~chad.smith/charms/precise/block-storage-broker/bsb-trusty-support into lp:~chad.smith/charms/precise/block-storage-broker/trunk
- Precise Pangolin (12.04)
- bsb-trusty-support
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Chad Smith | Disapprove | ||
Alberto Donato (community) | Needs Fixing | ||
Review via email: mp+211963@code.launchpad.net |
Commit message
Description of the change
This branch does a couple things:
1. stops the block-storage-
2. To handle the fact that euca-2.X doesn't report volume TAG responses in the euca-describe-
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.
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-
- 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
Chad Smith (chad.smith) wrote : | # |
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
Alberto Donato (ack) wrote : | # |
As discussed on IRC, I think the code could be refactored to reduce duplication, for example as in https:/
Tests could then mock _run_command rather than subprocess.
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_
+ data["instance_id"] = ""
+ data["tags"] = {"volume_name": ""}
These can be set with a single data.update()
#2:
+ volume_
+ volume_
Same as #1
#3:
+ for tag_volume_id in volume_
+ additional_tags = volume_
+ 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).
- 90. By Chad Smith
-
pull in ack's _run_command patch for simplification
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.
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.describein stances directly
Preview Diff
1 | === modified file 'hooks/test_util.py' |
2 | --- hooks/test_util.py 2014-03-21 17:42:00 +0000 |
3 | +++ hooks/test_util.py 2014-08-08 02:34:47 +0000 |
4 | @@ -1,5 +1,9 @@ |
5 | import util |
6 | -from util import StorageServiceUtil, ENVIRONMENT_MAP, generate_volume_label |
7 | +from util import ( |
8 | + StorageServiceUtil, EUCA2_INSTANCE_RESPONSE_LENGTH_V3, |
9 | + EUCA2_INSTANCE_RESPONSE_LENGTH_V2, parse_ec2_tag_response, |
10 | + parse_ec2_volume_response, parse_ec2_instance_response, |
11 | + parse_ec2_attachment_response, ENVIRONMENT_MAP, generate_volume_label) |
12 | import mocker |
13 | import os |
14 | import subprocess |
15 | @@ -9,7 +13,6 @@ |
16 | class TestNovaUtil(mocker.MockerTestCase): |
17 | |
18 | def setUp(self): |
19 | - super(TestNovaUtil, self).setUp() |
20 | self.maxDiff = None |
21 | util.hookenv = TestHookenv( |
22 | {"key": "myusername", "tenant": "myusername_project", |
23 | @@ -880,59 +883,9 @@ |
24 | message, util.hookenv._log_INFO, "Not logged- %s" % message) |
25 | |
26 | |
27 | -class MockEucaCommand(object): |
28 | - def __init__(self, result): |
29 | - self.result = result |
30 | - |
31 | - def main(self): |
32 | - return self.result |
33 | - |
34 | - |
35 | -class MockEucaReservation(object): |
36 | - def __init__(self, instances): |
37 | - self.instances = instances |
38 | - self.id = 1 |
39 | - |
40 | - |
41 | -class MockEucaInstance(object): |
42 | - def __init__(self, instance_id=None, ip_address=None, image_id=None, |
43 | - instance_type=None, kernel=None, private_dns_name=None, |
44 | - public_dns_name=None, state=None, tags=[], |
45 | - availability_zone=None): |
46 | - self.id = instance_id |
47 | - self.ip_address = ip_address |
48 | - self.image_id = image_id |
49 | - self.instance_type = instance_type |
50 | - self.kernel = kernel |
51 | - self.private_dns_name = private_dns_name |
52 | - self.public_dns_name = public_dns_name |
53 | - self.state = state |
54 | - self.tags = tags |
55 | - self.placement = availability_zone |
56 | - |
57 | - |
58 | -class MockAttachData(object): |
59 | - def __init__(self, device, instance_id): |
60 | - self.device = device |
61 | - self.instance_id = instance_id |
62 | - |
63 | - |
64 | -class MockVolume(object): |
65 | - def __init__(self, vol_id, device, instance_id, zone, size, status, |
66 | - snapshot_id, tags): |
67 | - self.id = vol_id |
68 | - self.attach_data = MockAttachData(device, instance_id) |
69 | - self.zone = zone |
70 | - self.size = size |
71 | - self.status = status |
72 | - self.snapshot_id = snapshot_id |
73 | - self.tags = tags |
74 | - |
75 | - |
76 | class TestEC2Util(mocker.MockerTestCase): |
77 | |
78 | def setUp(self): |
79 | - super(TestEC2Util, self).setUp() |
80 | self.maxDiff = None |
81 | util.hookenv = TestHookenv( |
82 | {"key": "ec2key", "secret": "ec2password", |
83 | @@ -1005,8 +958,8 @@ |
84 | configuration options. |
85 | """ |
86 | command = "euca-describe-instances" |
87 | - nova_cmd = self.mocker.replace(subprocess.check_call) |
88 | - nova_cmd(command, shell=True) |
89 | + euca_cmd = self.mocker.replace(subprocess.check_call) |
90 | + euca_cmd(command, shell=True) |
91 | self.mocker.replay() |
92 | |
93 | self.storage.validate_credentials() |
94 | @@ -1242,39 +1195,47 @@ |
95 | |
96 | def test_wb_ec2_describe_volumes_command_error(self): |
97 | """ |
98 | - L{_ec2_describe_volumes} will exit in error when the euca2ools |
99 | - C{DescribeVolumes} command fails. |
100 | + L{_ec2_describe_volumes} will exit in error when the |
101 | + C{euca-describe-volumes} command fails. |
102 | """ |
103 | - euca_command = self.mocker.replace(self.storage.ec2_volume_class) |
104 | - euca_command() |
105 | - self.mocker.throw(SystemExit(1)) |
106 | + command = "euca-describe-volumes" |
107 | + euca_volumes = self.mocker.replace(subprocess.check_output) |
108 | + euca_volumes(command, shell=True) |
109 | + self.mocker.throw(subprocess.CalledProcessError(1, command)) |
110 | self.mocker.replay() |
111 | |
112 | result = self.assertRaises( |
113 | SystemExit, self.storage._ec2_describe_volumes) |
114 | self.assertEqual(result.code, 1) |
115 | - message = "ERROR: Couldn't contact EC2 using euca-describe-volumes" |
116 | + message = ( |
117 | + "ERROR: Command '%s' returned non-zero exit status 1" % command) |
118 | self.assertIn( |
119 | message, util.hookenv._log_ERROR, "Not logged- %s" % message) |
120 | |
121 | def test_wb_ec2_describe_volumes_without_attached_instances(self): |
122 | """ |
123 | - L{_ec2_describe_volumes} parses the results of euca2ools |
124 | - C{DescribeVolumes} to create a C{dict} of volume information. When no |
125 | - C{instance_id}s are present the volumes are not attached so no |
126 | - C{device} or C{instance_id} information will be present. |
127 | + L{_ec2_describe_volumes} parses the output from the |
128 | + C{euca-describe-volumes} command and returns a C{dict} of volume |
129 | + information. When no C{ATTACHMENT} response types are present the |
130 | + volumes are not attached so no C{device} or C{instance_id} information |
131 | + will be present. Any C{TAG} response types will be added to the |
132 | + associated volume. |
133 | """ |
134 | - volume1 = MockVolume( |
135 | - "123-123-123", device="/dev/notshown", instance_id="notseen", |
136 | - zone="ec2-az1", size="10", status="available", |
137 | - snapshot_id="some-shot", tags={}) |
138 | - volume2 = MockVolume( |
139 | - "456-456-456", device="/dev/notshown", instance_id="notseen", |
140 | - zone="ec2-az2", size="8", status="available", |
141 | - snapshot_id="some-shot", tags={"volume_name": "my volume name"}) |
142 | - euca_command = self.mocker.replace(self.storage.ec2_volume_class) |
143 | - euca_command() |
144 | - self.mocker.result(MockEucaCommand([volume1, volume2])) |
145 | + volume_output = ( |
146 | + "VOLUME\t123-123-123\t 10\tsome-shot\tec2-az1\tavailable\t" |
147 | + "2014-03-19T22:00:02.580Z\nVOLUME\t456-456-456\t 8\tsome-shot\t" |
148 | + "ec2-az2\tavailable\t2014-03-19T22:00:02.580Z\n" |
149 | + "TAG\tvolume\t456-456-456\tvolume_name\tmy volume name") |
150 | + volume_command = "euca-describe-volumes" |
151 | + tag_output = ( |
152 | + "TAG\t456-456-456\tvolume\tother_key\tother value") |
153 | + tag_command = "euca-describe-tags" |
154 | + |
155 | + euca_command = self.mocker.replace(subprocess.check_output) |
156 | + euca_command(volume_command, shell=True) |
157 | + self.mocker.result(volume_output) |
158 | + euca_command(tag_command, shell=True) |
159 | + self.mocker.result(tag_output) |
160 | self.mocker.replay() |
161 | |
162 | expected = {"123-123-123": {"id": "123-123-123", "status": "available", |
163 | @@ -1290,27 +1251,30 @@ |
164 | "volume_label": "my volume name", |
165 | "size": "8", "instance_id": "", |
166 | "snapshot_id": "some-shot", |
167 | - "tags": {"volume_name": "my volume name"}}} |
168 | + "tags": {"volume_name": "my volume name", |
169 | + "other_key": "other value"}}} |
170 | self.assertEqual(self.storage._ec2_describe_volumes(), expected) |
171 | |
172 | def test_wb_ec2_describe_volumes_matches_volume_id_supplied(self): |
173 | """ |
174 | - L{_ec2_describe_volumes} parses the results of euca2ools |
175 | - C{DescribeVolumes} to create a C{dict} of volume information. |
176 | - When C{volume_id} is provided return a C{dict} for the matched volume. |
177 | + L{_ec2_describe_volumes} parses the output of the |
178 | + C{euca-describe-volumes} command and returns a C{dict} of volume |
179 | + information. When C{volume_id} is provided, return a C{dict} for the |
180 | + matched volume. |
181 | """ |
182 | volume_id = "123-123-123" |
183 | - volume1 = MockVolume( |
184 | - volume_id, device="/dev/notshown", instance_id="notseen", |
185 | - zone="ec2-az1", size="10", status="available", |
186 | - snapshot_id="some-shot", tags={}) |
187 | - volume2 = MockVolume( |
188 | - "456-456-456", device="/dev/notshown", instance_id="notseen", |
189 | - zone="ec2-az2", size="8", status="available", |
190 | - snapshot_id="some-shot", tags={"volume_name": "my volume name"}) |
191 | - euca_command = self.mocker.replace(self.storage.ec2_volume_class) |
192 | - euca_command() |
193 | - self.mocker.result(MockEucaCommand([volume1, volume2])) |
194 | + |
195 | + output = ( |
196 | + "VOLUME\t123-123-123\t 10\tsome-shot\tec2-az1\tavailable\t" |
197 | + "2014-03-19T22:00:02.580Z\nVOLUME\t456-456-456\t 8\tsome-shot\t" |
198 | + "ec2-az2\tavailable\t2014-03-19T22:00:02.580Z\n") |
199 | + command = "euca-describe-volumes" |
200 | + |
201 | + euca_command = self.mocker.replace(subprocess.check_output) |
202 | + euca_command(command, shell=True) |
203 | + self.mocker.result(output) |
204 | + euca_command("euca-describe-tags", shell=True) |
205 | + self.mocker.result("\n") |
206 | self.mocker.replay() |
207 | |
208 | expected = { |
209 | @@ -1323,18 +1287,22 @@ |
210 | |
211 | def test_wb_ec2_describe_volumes_unmatched_volume_id_supplied(self): |
212 | """ |
213 | - L{_ec2_describe_volumes} parses the results of euca2ools |
214 | - C{DescribeVolumes} to create a C{dict} of volume information. |
215 | - When C{volume_id} is provided and unmatched, return an empty C{dict}. |
216 | + L{_ec2_describe_volumes} parses the output of the |
217 | + C{euca-describe-volumes} command and returns a C{dict} of volume |
218 | + information. When C{volume_id} is provided and unmatched, return an |
219 | + empty C{dict}. |
220 | """ |
221 | unmatched_volume_id = "456-456-456" |
222 | - volume1 = MockVolume( |
223 | - "123-123-123", device="/dev/notshown", instance_id="notseen", |
224 | - zone="ec2-az1", size="10", status="available", |
225 | - snapshot_id="some-shot", tags={}) |
226 | - euca_command = self.mocker.replace(self.storage.ec2_volume_class) |
227 | - euca_command() |
228 | - self.mocker.result(MockEucaCommand([volume1])) |
229 | + output = ( |
230 | + "VOLUME\t123-123-123\t 10\tsome-shot\tec2-az1\tavailable\t" |
231 | + "2014-03-19T22:00:02.580Z\n") |
232 | + command = "euca-describe-volumes" |
233 | + |
234 | + euca_command = self.mocker.replace(subprocess.check_output) |
235 | + euca_command(command, shell=True) |
236 | + self.mocker.result(output) |
237 | + euca_command("euca-describe-tags", shell=True) |
238 | + self.mocker.result("\n") |
239 | self.mocker.replay() |
240 | |
241 | self.assertEqual( |
242 | @@ -1342,22 +1310,28 @@ |
243 | |
244 | def test_wb_ec2_describe_volumes_with_attached_instances(self): |
245 | """ |
246 | - L{_ec2_describe_volumes} parses the results of euca2ools |
247 | - C{DescribeVolumes} to create a C{dict} of volume information. If |
248 | - C{status} is C{in-use}, both C{device} and C{instance_id} will be |
249 | - returned in the C{dict}. |
250 | + L{_ec2_describe_volumes} parses the output of the |
251 | + C{euca-describe-volumes} command and returns a C{dict} of volume |
252 | + information. When C{status} is C{in-use}, both C{device} and |
253 | + C{instance_id} will be returned in the C{dict}. |
254 | """ |
255 | - volume1 = MockVolume( |
256 | - "123-123-123", device="/dev/notshown", instance_id="notseen", |
257 | - zone="ec2-az1", size="10", status="available", |
258 | - snapshot_id="some-shot", tags={}) |
259 | - volume2 = MockVolume( |
260 | - "456-456-456", device="/dev/xvdc", instance_id="i-456456", |
261 | - zone="ec2-az2", size="8", status="in-use", |
262 | - snapshot_id="some-shot", tags={"volume_name": "my volume name"}) |
263 | - euca_command = self.mocker.replace(self.storage.ec2_volume_class) |
264 | - euca_command() |
265 | - self.mocker.result(MockEucaCommand([volume1, volume2])) |
266 | + output = ( |
267 | + "VOLUME\t123-123-123\t 10\tsome-shot\tec2-az1\tavailable\t" |
268 | + "2014-03-19T22:00:02.580Z\nVOLUME\t456-456-456\t 8\tsome-shot\t" |
269 | + "ec2-az2\tin-use\t2014-03-19T22:00:02.580Z\nATTACHMENT\t" |
270 | + "456-456-456\ti-456456\t/dev/xvdc\tattached\t" |
271 | + "2014-03-19T22:00:02.000Z\nTAG\tvolume\t456-456-456\tvolume_name\t" |
272 | + "my volume name\n") |
273 | + command = "euca-describe-volumes" |
274 | + tag_output = ( |
275 | + "TAG\t456-456-456\tvolume\tvolume_name\tmy volume name") |
276 | + tag_command = "euca-describe-tags" |
277 | + |
278 | + euca_command = self.mocker.replace(subprocess.check_output) |
279 | + euca_command(command, shell=True) |
280 | + self.mocker.result(output) |
281 | + euca_command(tag_command, shell=True) |
282 | + self.mocker.result(tag_output) |
283 | self.mocker.replay() |
284 | |
285 | expected = {"123-123-123": {"id": "123-123-123", "status": "available", |
286 | @@ -1377,6 +1351,47 @@ |
287 | self.assertEqual( |
288 | self.storage._ec2_describe_volumes(), expected) |
289 | |
290 | + def test_wb_ec2_describe_volumes_orphaned_tags(self): |
291 | + """ |
292 | + L{_ec2_describe_volumes} parses the output of the |
293 | + C{euca-describe-volumes} and augments that data with volume-related |
294 | + tags from C{euca-describe-tags}. When C{euca-describe-tags} returns |
295 | + information about orphaned tags for volumes that no longer exist, |
296 | + L{_ec2_describe_volumes} will log this information and move on. |
297 | + """ |
298 | + output = ( |
299 | + "VOLUME\t456-456-456\t 8\tsome-shot\t" |
300 | + "ec2-az2\tin-use\t2014-03-19T22:00:02.580Z\nATTACHMENT\t" |
301 | + "456-456-456\ti-456456\t/dev/xvdc\tattached\t" |
302 | + "2014-03-19T22:00:02.000Z\nTAG\tvolume\t456-456-456\tvolume_name\t" |
303 | + "my volume name\n") |
304 | + command = "euca-describe-volumes" |
305 | + tag_output = ( |
306 | + "TAG\tvolume-not-here\tvolume\tvolume_name\tmy volume name\n" |
307 | + "TAG\t456-456-456\tvolume\tvolume_name\tmy volume name") |
308 | + tag_command = "euca-describe-tags" |
309 | + |
310 | + euca_command = self.mocker.replace(subprocess.check_output) |
311 | + euca_command(command, shell=True) |
312 | + self.mocker.result(output) |
313 | + euca_command(tag_command, shell=True) |
314 | + self.mocker.result(tag_output) |
315 | + self.mocker.replay() |
316 | + |
317 | + expected = {"456-456-456": {"id": "456-456-456", "status": "in-use", |
318 | + "device": "/dev/xvdc", |
319 | + "availability_zone": "ec2-az2", |
320 | + "volume_label": "my volume name", |
321 | + "size": "8", "instance_id": "i-456456", |
322 | + "snapshot_id": "some-shot", |
323 | + "tags": {"volume_name": "my volume name"}}} |
324 | + self.assertEqual( |
325 | + self.storage._ec2_describe_volumes(), expected) |
326 | + message = ( |
327 | + "Ignoring tags for volume-id volume-not-here that doesn't exist") |
328 | + self.assertIn( |
329 | + message, util.hookenv._log_INFO, "Not logged- %s" % message) |
330 | + |
331 | def test_wb_ec2_create_volume(self): |
332 | """ |
333 | L{_ec2_create_volume} uses the command C{euca-create-volume} to create |
334 | @@ -1392,18 +1407,17 @@ |
335 | zone = "ec2-az3" |
336 | command = "euca-create-volume -z %s -s %s" % (zone, size) |
337 | |
338 | - reservation = MockEucaReservation( |
339 | - [MockEucaInstance( |
340 | - instance_id=instance_id, availability_zone=zone)]) |
341 | - euca_command = self.mocker.replace(self.storage.ec2_instance_class) |
342 | - euca_command() |
343 | - self.mocker.result(MockEucaCommand([reservation])) |
344 | create = self.mocker.replace(subprocess.check_output) |
345 | create(command, shell=True) |
346 | self.mocker.result( |
347 | "VOLUME %s 9 nova creating 2014-03-14T17:26:20\n" % volume_id) |
348 | self.mocker.replay() |
349 | |
350 | + def mock_describe_instances(my_id): |
351 | + self.assertEqual(my_id, instance_id) |
352 | + return {"availability_zone": zone} |
353 | + self.storage._ec2_describe_instances = mock_describe_instances |
354 | + |
355 | def mock_describe_volumes(my_id): |
356 | self.assertEqual(my_id, volume_id) |
357 | return {"id": volume_id} |
358 | @@ -1436,10 +1450,10 @@ |
359 | volume_label = "postgresql/0 unit volume" |
360 | size = 10 |
361 | |
362 | - euca_command = self.mocker.replace(self.storage.ec2_instance_class) |
363 | - euca_command() |
364 | - self.mocker.result(MockEucaCommand([])) # Empty results from euca |
365 | - self.mocker.replay() |
366 | + def mock_describe_instances(my_id): |
367 | + self.assertEqual(my_id, instance_id) |
368 | + return {} # No results found |
369 | + self.storage._ec2_describe_instances = mock_describe_instances |
370 | |
371 | result = self.assertRaises( |
372 | SystemExit, self.storage._ec2_create_volume, size, volume_label, |
373 | @@ -1465,17 +1479,16 @@ |
374 | zone = "ec2-az3" |
375 | command = "euca-create-volume -z %s -s %s" % (zone, size) |
376 | |
377 | - reservation = MockEucaReservation( |
378 | - [MockEucaInstance( |
379 | - instance_id=instance_id, availability_zone=zone)]) |
380 | - euca_command = self.mocker.replace(self.storage.ec2_instance_class) |
381 | - euca_command() |
382 | - self.mocker.result(MockEucaCommand([reservation])) |
383 | create = self.mocker.replace(subprocess.check_output) |
384 | create(command, shell=True) |
385 | self.mocker.result("INSTANCE invalid-instance-type-response\n") |
386 | self.mocker.replay() |
387 | |
388 | + def mock_describe_instances(my_id): |
389 | + self.assertEqual(my_id, instance_id) |
390 | + return {"availability_zone": zone} |
391 | + self.storage._ec2_describe_instances = mock_describe_instances |
392 | + |
393 | def mock_describe_volumes(my_id): |
394 | raise Exception("_ec2_describe_volumes should not be called") |
395 | self.storage._ec2_describe_volumes = mock_describe_volumes |
396 | @@ -1507,18 +1520,17 @@ |
397 | zone = "ec2-az3" |
398 | command = "euca-create-volume -z %s -s %s" % (zone, size) |
399 | |
400 | - reservation = MockEucaReservation( |
401 | - [MockEucaInstance( |
402 | - instance_id=instance_id, availability_zone=zone)]) |
403 | - euca_command = self.mocker.replace(self.storage.ec2_instance_class) |
404 | - euca_command() |
405 | - self.mocker.result(MockEucaCommand([reservation])) |
406 | create = self.mocker.replace(subprocess.check_output) |
407 | create(command, shell=True) |
408 | self.mocker.result( |
409 | "VOLUME %s 9 nova creating 2014-03-14T17:26:20\n" % volume_id) |
410 | self.mocker.replay() |
411 | |
412 | + def mock_describe_instances(my_id): |
413 | + self.assertEqual(my_id, instance_id) |
414 | + return {"availability_zone": zone} |
415 | + self.storage._ec2_describe_instances = mock_describe_instances |
416 | + |
417 | def mock_describe_volumes(my_id): |
418 | self.assertEqual(my_id, volume_id) |
419 | return {} # No details found for this volume |
420 | @@ -1548,17 +1560,16 @@ |
421 | zone = "ec2-az3" |
422 | command = "euca-create-volume -z %s -s %s" % (zone, size) |
423 | |
424 | - reservation = MockEucaReservation( |
425 | - [MockEucaInstance( |
426 | - instance_id=instance_id, availability_zone=zone)]) |
427 | - euca_command = self.mocker.replace(self.storage.ec2_instance_class) |
428 | - euca_command() |
429 | - self.mocker.result(MockEucaCommand([reservation])) |
430 | create = self.mocker.replace(subprocess.check_output) |
431 | create(command, shell=True) |
432 | self.mocker.throw(subprocess.CalledProcessError(1, command)) |
433 | self.mocker.replay() |
434 | |
435 | + def mock_describe_instances(my_id): |
436 | + self.assertEqual(my_id, instance_id) |
437 | + return {"availability_zone": zone} |
438 | + self.storage._ec2_describe_instances = mock_describe_instances |
439 | + |
440 | def mock_exception(my_id): |
441 | raise Exception("These methods should not be called") |
442 | self.storage._ec2_describe_volumes = mock_exception |
443 | @@ -1728,3 +1739,243 @@ |
444 | (volume_id, instance_id)) |
445 | self.assertIn( |
446 | message, util.hookenv._log_INFO, "Not logged- %s" % message) |
447 | + |
448 | + def test_wb_ec2_describe_instances_euca_v2(self): |
449 | + """ |
450 | + L{_ec2_describe_instances} parses the output of the command |
451 | + C{euca-describe-instances} for euca2ools version 2.X and returns a |
452 | + C{dict} of instance information. |
453 | + """ |
454 | + command = "euca-describe-instances" |
455 | + output = ( |
456 | + "INSTANCE\ti-aaa\tami-aaa\taaa.amazonaws.com\t" |
457 | + "aaa.ec2.internal\trunning\t\t0\t\tm1.small\t" |
458 | + "2014-03-19T17:52:02.000Z\tus-east-1a\taki-aaa\t\t\t" |
459 | + "monitoring-disabled\t54.81.8.9\t10.138.61.2\t\t\tebs\t\t\t\t\t" |
460 | + "paravirtual\t\t\t\t") |
461 | + |
462 | + ec2_list = self.mocker.replace(subprocess.check_output) |
463 | + ec2_list(command, shell=True) |
464 | + self.mocker.result(output) |
465 | + self.mocker.replay() |
466 | + |
467 | + expected = { |
468 | + "i-aaa": { |
469 | + "ip-address": "54.81.8.9", "private-ip-address": "10.138.61.2", |
470 | + "kernel": "aki-aaa", "instance-type": "m1.small", |
471 | + "state": "running", "public-dns-name": "aaa.amazonaws.com", |
472 | + "private-dns-name": "aaa.ec2.internal", "instance_id": "i-aaa", |
473 | + "image-id": "ami-aaa", "availability_zone": "us-east-1a"}} |
474 | + |
475 | + self.assertEqual( |
476 | + EUCA2_INSTANCE_RESPONSE_LENGTH_V2, len(output.split("\t"))) |
477 | + |
478 | + self.assertEqual( |
479 | + self.storage._ec2_describe_instances(), expected) |
480 | + |
481 | + def test_wb_ec2_describe_instances_euca_invalid_output_version(self): |
482 | + """ |
483 | + L{_ec2_describe_instances} parses the output of the command |
484 | + C{euca-describe-instances} and if it sees an output length that is |
485 | + unsupported, an error is logged. |
486 | + """ |
487 | + command = "euca-describe-instances" |
488 | + output = ( |
489 | + "INSTANCE\ti-aaa\tami-0b9c9f62\tblah.amazonaws.com\t" |
490 | + "blah.ec2.internal\trunning\t\t0\t") |
491 | + |
492 | + ec2_list = self.mocker.replace(subprocess.check_output) |
493 | + ec2_list(command, shell=True) |
494 | + self.mocker.result(output) |
495 | + self.mocker.replay() |
496 | + |
497 | + self.assertNotIn( |
498 | + len(output.split("\t")), |
499 | + [EUCA2_INSTANCE_RESPONSE_LENGTH_V3, |
500 | + EUCA2_INSTANCE_RESPONSE_LENGTH_V3]) |
501 | + |
502 | + result = self.assertRaises( |
503 | + SystemExit, self.storage._ec2_describe_instances) |
504 | + self.assertEqual(result.code, 1) |
505 | + message = "ERROR: Unsupported euca INSTANCE output format version" |
506 | + self.assertIn( |
507 | + message, util.hookenv._log_ERROR, "Not logged- %s" % message) |
508 | + |
509 | + def test_wb_ec2_describe_instances_specific_instance_id(self): |
510 | + """ |
511 | + L{_ec2_describe_instances} parses the output of the command |
512 | + C{euca-describe-instances} for euca2ools version 3.X and returns a |
513 | + C{dict} of instance information. When C{instance_id} is provided, |
514 | + results are filtered to only the matching C{instance_id}. |
515 | + """ |
516 | + command = "euca-describe-instances" |
517 | + output = ( |
518 | + "INSTANCE\ti-aaa\tami-aaa\taaa.amazonaws.com\t" |
519 | + "aaa.ec2.internal\trunning\t\t0\t\tm1.small\t" |
520 | + "2014-03-19T17:52:02.000Z\tus-east-1a\taki-aaa\t\t\t" |
521 | + "monitoring-disabled\t54.81.8.1\t10.138.61.1\t\t\tebs" |
522 | + "\t\t\t\t\tparavirtual\txen\t" |
523 | + "5926eb12c796888691d9fa135f6e6f8ada297a9b1ecbea0c12282522400636f3" |
524 | + "\tsg-55e2e33e,sg-29e2e342\tdefault\tfalse\t\n" |
525 | + "INSTANCE\ti-bbb\tami-bbb\tbbb.amazonaws.com\t" |
526 | + "bbb.ec2.internal\trunning\t\t0\t\tm1.small\t" |
527 | + "2014-03-19T17:52:02.000Z\tus-east-1a\taki-bbb\t\t\t" |
528 | + "monitoring-disabled\t54.81.8.2\t10.138.61.2\t\t\tebs" |
529 | + "\t\t\t\t\tparavirtual\txen\t" |
530 | + "5926eb12c796888691d9fa135f6e6f8ada297a9b1ecbea0c12282522400636f3" |
531 | + "\tsg-55e2e33e,sg-29e2e342\tdefault\tfalse\t") |
532 | + |
533 | + ec2_list = self.mocker.replace(subprocess.check_output) |
534 | + ec2_list(command, shell=True) |
535 | + self.mocker.result(output) |
536 | + self.mocker.replay() |
537 | + |
538 | + expected = { |
539 | + "ip-address": "54.81.8.1", "private-ip-address": "10.138.61.1", |
540 | + "kernel": "aki-aaa", "instance-type": "m1.small", |
541 | + "state": "running", "public-dns-name": "aaa.amazonaws.com", |
542 | + "private-dns-name": "aaa.ec2.internal", "instance_id": "i-aaa", |
543 | + "image-id": "ami-aaa", "availability_zone": "us-east-1a"} |
544 | + |
545 | + self.assertEqual( |
546 | + EUCA2_INSTANCE_RESPONSE_LENGTH_V3, |
547 | + len(output.split("\n")[0].split("\t"))) |
548 | + |
549 | + self.assertEqual( |
550 | + self.storage._ec2_describe_instances("i-aaa"), expected) |
551 | + |
552 | + def test_parse_ec2_tag_response_not_tag_response(self): |
553 | + """ |
554 | + L{parse_ec2_tag_response} returns an empty C{dict} when the |
555 | + line does not represent a C{TAG} response type. |
556 | + """ |
557 | + non_tag_output_lines = ( |
558 | + "VOLUME\t123-123-123\t 10\tsome-shot\tec2-az1\tavailable\t" |
559 | + "2014-03-19T22:00:02.580Z\n" |
560 | + "ATTACHMENT\t123-123-123\ti-123123\t/dev/xvdc\tattached\t" |
561 | + "2014-03-19T22:00:02.000Z\n" |
562 | + "INSTANCE\ti-aaa\tami-aaa\taaa.amazonaws.com\t" |
563 | + "aaa.ec2.internal\trunning\t\t0\t\tm1.small\t" |
564 | + "2014-03-19T17:52:02.000Z\tus-east-1a\taki-aaa\t\t\t" |
565 | + "monitoring-disabled\t54.81.8.1\t10.138.61.1\t\t\tebs" |
566 | + "\t\t\t\t\tparavirtual\txen\t" |
567 | + "5926eb12c796888691d9fa135f6e6f8ada297a9b1ecbea0c12282522400636f3" |
568 | + "\tsg-55e2e33e,sg-29e2e342\tdefault\tfalse\t") |
569 | + for line in non_tag_output_lines.split("\n"): |
570 | + self.assertEqual(parse_ec2_tag_response(line), {}) |
571 | + |
572 | + def test_parse_ec2_attachment_response_not_attachment_response(self): |
573 | + """ |
574 | + L{parse_ec2_attachment_response} returns an empty C{dict} when the |
575 | + line does not represent a C{ATTACHMENT} response type. |
576 | + """ |
577 | + non_attachment_output_lines = ( |
578 | + "TAG\tvolume\t456-456-456\tvolume_name\tmy volume name\n" |
579 | + "VOLUME\t123-123-123\t 10\tsome-shot\tec2-az1\tavailable\t" |
580 | + "2014-03-19T22:00:02.580Z\n" |
581 | + "INSTANCE\ti-aaa\tami-aaa\taaa.amazonaws.com\t" |
582 | + "aaa.ec2.internal\trunning\t\t0\t\tm1.small\t" |
583 | + "2014-03-19T17:52:02.000Z\tus-east-1a\taki-aaa\t\t\t" |
584 | + "monitoring-disabled\t54.81.8.1\t10.138.61.1\t\t\tebs" |
585 | + "\t\t\t\t\tparavirtual\txen\t" |
586 | + "5926eb12c796888691d9fa135f6e6f8ada297a9b1ecbea0c12282522400636f3" |
587 | + "\tsg-55e2e33e,sg-29e2e342\tdefault\tfalse\t") |
588 | + for line in non_attachment_output_lines.split("\n"): |
589 | + self.assertEqual(parse_ec2_attachment_response(line), {}) |
590 | + |
591 | + def test_parse_ec2_volume_response_not_volume_response(self): |
592 | + """ |
593 | + L{parse_ec2_volume_response} returns an empty C{dict} when the |
594 | + line does not represent a C{VOLUME} response type. |
595 | + """ |
596 | + non_volume_output_lines = ( |
597 | + "TAG\tvolume\t456-456-456\tvolume_name\tmy volume name\n" |
598 | + "ATTACHMENT\t123-123-123\ti-123123\t/dev/xvdc\tattached\t" |
599 | + "2014-03-19T22:00:02.000Z\n" |
600 | + "INSTANCE\ti-aaa\tami-aaa\taaa.amazonaws.com\t" |
601 | + "aaa.ec2.internal\trunning\t\t0\t\tm1.small\t" |
602 | + "2014-03-19T17:52:02.000Z\tus-east-1a\taki-aaa\t\t\t" |
603 | + "monitoring-disabled\t54.81.8.1\t10.138.61.1\t\t\tebs" |
604 | + "\t\t\t\t\tparavirtual\txen\t" |
605 | + "5926eb12c796888691d9fa135f6e6f8ada297a9b1ecbea0c12282522400636f3" |
606 | + "\tsg-55e2e33e,sg-29e2e342\tdefault\tfalse\t") |
607 | + for line in non_volume_output_lines.split("\n"): |
608 | + self.assertEqual(parse_ec2_volume_response(line), {}) |
609 | + |
610 | + def test_parse_ec2_instance_response_not_instance_response(self): |
611 | + """ |
612 | + L{parse_ec2_instance_response} returns an empty C{dict} when the |
613 | + line does not represent a C{INSTANCE} response type. |
614 | + """ |
615 | + non_instance_output_lines = ( |
616 | + "TAG\tvolume\t456-456-456\tvolume_name\tmy volume name\n" |
617 | + "VOLUME\t123-123-123\t 10\tsome-shot\tec2-az1\tavailable\t" |
618 | + "2014-03-19T22:00:02.580Z\n" |
619 | + "ATTACHMENT\t123-123-123\ti-123123\t/dev/xvdc\tattached\t" |
620 | + "2014-03-19T22:00:02.000Z\n") |
621 | + for line in non_instance_output_lines.split("\n"): |
622 | + self.assertEqual(parse_ec2_instance_response(line), {}) |
623 | + |
624 | + def test_parse_ec2_instance_response(self): |
625 | + """ |
626 | + L{parse_ec2_instance_response} returns a C{dict} of the parsed |
627 | + C{INSTANCE} response type. |
628 | + """ |
629 | + output = ( |
630 | + "INSTANCE\ti-aaa\tami-aaa\taaa.amazonaws.com\t" |
631 | + "aaa.ec2.internal\trunning\t\t0\t\tm1.small\t" |
632 | + "2014-03-19T17:52:02.000Z\tus-east-1a\taki-aaa\t\t\t" |
633 | + "monitoring-disabled\t54.81.8.1\t10.138.61.1\t\t\tebs" |
634 | + "\t\t\t\t\tparavirtual\txen\t" |
635 | + "5926eb12c796888691d9fa135f6e6f8ada297a9b1ecbea0c12282522400636f3" |
636 | + "\tsg-55e2e33e,sg-29e2e342\tdefault\tfalse\t") |
637 | + |
638 | + expected = { |
639 | + "ip-address": "54.81.8.1", "private-ip-address": "10.138.61.1", |
640 | + "image-id": "ami-aaa", "instance-type": "m1.small", |
641 | + "kernel": "aki-aaa", "private-dns-name": "aaa.ec2.internal", |
642 | + "public-dns-name": "aaa.amazonaws.com", "state": "running", |
643 | + "availability_zone": "us-east-1a", "instance_id": "i-aaa"} |
644 | + |
645 | + self.assertEqual(parse_ec2_instance_response(output), expected) |
646 | + |
647 | + def test_parse_ec2_tag_response(self): |
648 | + """ |
649 | + L{parse_ec2_tag_response} returns a C{dict} of the parsed C{TAG} |
650 | + response type. |
651 | + """ |
652 | + output = "TAG\tvolume\t456-456-456\tvolume_name\tmy volume name" |
653 | + |
654 | + expected = { |
655 | + "tag_type": "volume", "id": "456-456-456", "key": "volume_name", |
656 | + "value": "my volume name"} |
657 | + self.assertEqual(parse_ec2_tag_response(output), expected) |
658 | + |
659 | + def test_parse_ec2_attachment_response(self): |
660 | + """ |
661 | + L{parse_ec2_attachment_response} returns a C{dict} of the parsed |
662 | + C{ATTACHMENT} response type. |
663 | + """ |
664 | + output = ( |
665 | + "ATTACHMENT\t123-123-123\ti-123123\t/dev/xvdc\tattached\t" |
666 | + "2014-03-19T22:00:02.000Z\n") |
667 | + |
668 | + expected = { |
669 | + "volume_id": "123-123-123", "instance_id": "i-123123", |
670 | + "device": "/dev/xvdc", "attach_status": "attached"} |
671 | + self.assertEqual(parse_ec2_attachment_response(output), expected) |
672 | + |
673 | + def test_parse_ec2_volume_response(self): |
674 | + """ |
675 | + L{parse_ec2_volume_response} returns a C{dict} of the parsed |
676 | + C{VOLUME} response type. |
677 | + """ |
678 | + output = ( |
679 | + "VOLUME\t123-123-123\t 10\tsome-shot\tec2-az1\tavailable\t" |
680 | + "2014-03-19T22:00:02.580Z\n") |
681 | + |
682 | + expected = { |
683 | + "id": "123-123-123", "size": "10", |
684 | + "snapshot_id": "some-shot", "availability_zone": "ec2-az1", |
685 | + "status": "available"} |
686 | + self.assertEqual(parse_ec2_volume_response(output), expected) |
687 | |
688 | === modified file 'hooks/util.py' |
689 | --- hooks/util.py 2014-03-21 17:39:48 +0000 |
690 | +++ hooks/util.py 2014-08-08 02:34:47 +0000 |
691 | @@ -23,6 +23,9 @@ |
692 | "nova": {"validate": "nova list", |
693 | "detach": "nova volume-detach %s %s"}} |
694 | |
695 | +EUCA2_INSTANCE_RESPONSE_LENGTH_V2 = 30 |
696 | +EUCA2_INSTANCE_RESPONSE_LENGTH_V3 = 32 |
697 | + |
698 | |
699 | class StorageServiceUtil(object): |
700 | """Interact with an underlying cloud storage provider. |
701 | @@ -45,11 +48,6 @@ |
702 | self.environment_map = ENVIRONMENT_MAP[provider] |
703 | self.commands = PROVIDER_COMMANDS[provider] |
704 | self.required_config_options = REQUIRED_CONFIG_OPTIONS[provider] |
705 | - if provider == "ec2": |
706 | - import euca2ools.commands.euca.describevolumes as getvolumes |
707 | - import euca2ools.commands.euca.describeinstances as getinstances |
708 | - self.ec2_volume_class = getvolumes.DescribeVolumes |
709 | - self.ec2_instance_class = getinstances.DescribeInstances |
710 | |
711 | def load_environment(self): |
712 | """ |
713 | @@ -212,83 +210,112 @@ |
714 | return |
715 | |
716 | # EC2-specific methods |
717 | + def _run_command(self, command): |
718 | + try: |
719 | + output = subprocess.check_call(command, shell=True) |
720 | + except subprocess.CalledProcessError, e: |
721 | + hookenv.log("ERROR: %s" % str(e), hookenv.ERROR) |
722 | + sys.exit(1) |
723 | + |
724 | + # Split the output by lines |
725 | + lines = [] |
726 | + if output: |
727 | + for line in output.split("\n"): |
728 | + line = line.strip() |
729 | + if line: |
730 | + lines.append(line) |
731 | + return lines |
732 | + |
733 | def _ec2_create_tag(self, volume_id, tag_name, tag_value=None): |
734 | """Attach a tag and optional C{tag_value} to the given C{volume_id}""" |
735 | tag_string = tag_name |
736 | if tag_value: |
737 | tag_string += "=%s" % tag_value |
738 | - command = 'euca-create-tags %s --tag "%s"' % (volume_id, tag_string) |
739 | + command = "euca-create-tags %s --tag \"%s\"" % (volume_id, tag_string) |
740 | |
741 | - try: |
742 | - subprocess.check_call(command, shell=True) |
743 | - except subprocess.CalledProcessError, e: |
744 | - hookenv.log( |
745 | - "ERROR: Couldn't add tags to the resource. %s" % str(e), |
746 | - hookenv.ERROR) |
747 | - sys.exit(1) |
748 | + self._run_command(command) |
749 | hookenv.log("Tagged (%s) to %s." % (tag_string, volume_id)) |
750 | |
751 | def _ec2_describe_instances(self, instance_id=None): |
752 | """ |
753 | - Use euca2ools libraries to describe instances and return a C{dict} |
754 | + Use euca2ools command output to describe instances and return a C{dict} |
755 | """ |
756 | result = {} |
757 | - try: |
758 | - command = self.ec2_instance_class() |
759 | - reservations = command.main() |
760 | - except SystemExit: |
761 | - hookenv.log( |
762 | - "ERROR: Couldn't contact EC2 using euca-describe-instances", |
763 | - hookenv.ERROR) |
764 | - sys.exit(1) |
765 | - for reservation in reservations: |
766 | - for inst in reservation.instances: |
767 | - result[inst.id] = { |
768 | - "ip-address": inst.ip_address, "image-id": inst.image_id, |
769 | - "instance-type": inst.image_id, "kernel": inst.kernel, |
770 | - "private-dns-name": inst.private_dns_name, |
771 | - "public-dns-name": inst.public_dns_name, |
772 | - "reservation-id": reservation.id, |
773 | - "state": inst.state, "tags": inst.tags, |
774 | - "availability_zone": inst.placement} |
775 | + |
776 | + lines = self._run_command("euca-describe-instances") |
777 | + for line in lines: |
778 | + response_type = line.split("\t")[0] |
779 | + if response_type != "INSTANCE": |
780 | + continue |
781 | + instance_data = parse_ec2_instance_response(line) |
782 | + result[instance_data["instance_id"]] = instance_data |
783 | + |
784 | if instance_id: |
785 | if instance_id in result: |
786 | return result[instance_id] |
787 | return {} |
788 | return result |
789 | |
790 | + def _ec2_describe_volume_tags(self): |
791 | + """ |
792 | + Parse output of C{euca-describe-tags} returning a C{dict} of volume ids |
793 | + and any associated tags. |
794 | + """ |
795 | + result = {} |
796 | + |
797 | + lines = self._run_command("euca-describe-tags") |
798 | + for line in lines: |
799 | + response_type = line.split("\t")[0] |
800 | + if response_type != "TAG": |
801 | + continue |
802 | + data = parse_ec2_tag_response(line) |
803 | + if not result.get(data["id"]): |
804 | + result[data["id"]] = {"tags": {}} |
805 | + result[data["id"]]["tags"].update({data["key"]: data["value"]}) |
806 | + return result |
807 | + |
808 | def _ec2_describe_volumes(self, volume_id=None): |
809 | """ |
810 | - Use euca2ools libraries to describe volumes and return a C{dict} |
811 | + Parse output of C{euca-describe-volumes} returning a C{dict} of volume |
812 | + ids all related volume information. |
813 | """ |
814 | result = {} |
815 | - try: |
816 | - command = self.ec2_volume_class() |
817 | - volumes = command.main() |
818 | - except SystemExit: |
819 | - hookenv.log( |
820 | - "ERROR: Couldn't contact EC2 using euca-describe-volumes", |
821 | - hookenv.ERROR) |
822 | - sys.exit(1) |
823 | - for volume in volumes: |
824 | - result[volume.id] = { |
825 | - "device": "", |
826 | - "instance_id": "", |
827 | - "size": volume.size, |
828 | - "snapshot_id": volume.snapshot_id, |
829 | - "status": volume.status, |
830 | - "tags": volume.tags, |
831 | - "id": volume.id, |
832 | - "availability_zone": volume.zone} |
833 | - if "volume_name" in volume.tags: |
834 | - result[volume.id]["volume_label"] = volume.tags["volume_name"] |
835 | - else: |
836 | - result[volume.id]["tags"]["volume_name"] = "" |
837 | - result[volume.id]["volume_label"] = "" |
838 | - if volume.status == "in-use": |
839 | - result[volume.id]["instance_id"] = ( |
840 | - volume.attach_data.instance_id) |
841 | - result[volume.id]["device"] = volume.attach_data.device |
842 | + |
843 | + lines = self._run_command("euca-describe-volumes") |
844 | + for line in lines: |
845 | + response_type = line.split("\t")[0] |
846 | + if response_type == "VOLUME": |
847 | + data = parse_ec2_volume_response(line) |
848 | + data["device"] = "" |
849 | + data["volume_label"] = "" |
850 | + data["instance_id"] = "" |
851 | + data["tags"] = {"volume_name": ""} |
852 | + result[data["id"]] = data |
853 | + elif response_type == "TAG": |
854 | + data = parse_ec2_tag_response(line) |
855 | + tags = result[data["id"]]["tags"] |
856 | + tags.update({data["key"]: data["value"]}) |
857 | + volume_name = tags.get("volume_name") |
858 | + if volume_name: |
859 | + result[data["id"]]["volume_label"] = volume_name |
860 | + elif response_type == "ATTACHMENT": |
861 | + data = parse_ec2_attachment_response(line) |
862 | + volume_data = result[data["volume_id"]] |
863 | + volume_data.update({"instance_id": data["instance_id"]}) |
864 | + volume_data.update({"device": data["device"]}) |
865 | + else: |
866 | + continue |
867 | + |
868 | + volume_tag_data = self._ec2_describe_volume_tags() |
869 | + for tag_volume_id in volume_tag_data.keys(): |
870 | + additional_tags = volume_tag_data[tag_volume_id]["tags"] |
871 | + if tag_volume_id not in result: |
872 | + hookenv.log( |
873 | + "Ignoring tags for volume-id %s that doesn't exist" % |
874 | + tag_volume_id) |
875 | + else: |
876 | + result[tag_volume_id]["tags"].update(additional_tags) |
877 | + |
878 | if volume_id: |
879 | if volume_id in result: |
880 | return result[volume_id] |
881 | @@ -311,19 +338,15 @@ |
882 | (instance_id, config_data["endpoint"]), hookenv.ERROR) |
883 | sys.exit(1) |
884 | |
885 | - try: |
886 | - output = subprocess.check_output( |
887 | - "euca-create-volume -z %s -s %s" % |
888 | - (instance["availability_zone"], size), shell=True) |
889 | - except subprocess.CalledProcessError, e: |
890 | - hookenv.log("ERROR: %s" % str(e), hookenv.ERROR) |
891 | - sys.exit(1) |
892 | + command = "euca-create-volume -z %s -s %s" % ( |
893 | + instance["availability_zone"], size) |
894 | |
895 | - response_type, volume_id = output.split()[:2] |
896 | + lines = self._run_command(command) |
897 | + response_type, volume_id = lines[:2] |
898 | if response_type != "VOLUME": |
899 | hookenv.log( |
900 | "ERROR: Didn't get VOLUME response from euca-create-volume. " |
901 | - "Response: %s" % output, hookenv.ERROR) |
902 | + "Response: %s" % "".join(lines), hookenv.ERROR) |
903 | sys.exit(1) |
904 | volume = self.describe_volumes(volume_id.strip()) |
905 | if not volume: |
906 | @@ -341,13 +364,10 @@ |
907 | the device path. |
908 | """ |
909 | device = "/dev/xvdc" |
910 | - try: |
911 | - subprocess.check_call( |
912 | - "euca-attach-volume -i %s -d %s %s" % |
913 | - (instance_id, device, volume_id), shell=True) |
914 | - except subprocess.CalledProcessError, e: |
915 | - hookenv.log("ERROR: %s" % str(e), hookenv.ERROR) |
916 | - sys.exit(1) |
917 | + command = "euca-attach-volume -i %s -d %s %s" % ( |
918 | + instance_id, device, volume_id) |
919 | + |
920 | + self._run_command(command) |
921 | return device |
922 | |
923 | # Nova-specific methods |
924 | @@ -358,17 +378,9 @@ |
925 | """ |
926 | from ast import literal_eval |
927 | result = {"tags": {}, "instance_id": "", "device": ""} |
928 | - command = "nova volume-show '%s'" % volume_id |
929 | - try: |
930 | - output = subprocess.check_output(command, shell=True) |
931 | - except subprocess.CalledProcessError, e: |
932 | - hookenv.log( |
933 | - "ERROR: Failed to get nova volume info. %s" % str(e), |
934 | - hookenv.ERROR) |
935 | - sys.exit(1) |
936 | - for line in output.split("\n"): |
937 | - if not line.strip(): # Skip empty lines |
938 | - continue |
939 | + |
940 | + lines = self._run_command("nova volume-show '%s'" % volume_id) |
941 | + for line in lines: |
942 | if "+----" in line or "Property" in line: |
943 | continue |
944 | (_, key, value, _) = line.split("|") |
945 | @@ -393,15 +405,9 @@ |
946 | def _nova_describe_volumes(self, volume_id=None): |
947 | """Create a C{dict} describing all nova volumes""" |
948 | result = {} |
949 | - command = "nova volume-list" |
950 | - try: |
951 | - output = subprocess.check_output(command, shell=True) |
952 | - except subprocess.CalledProcessError, e: |
953 | - hookenv.log("ERROR: %s" % str(e), hookenv.ERROR) |
954 | - sys.exit(1) |
955 | - for line in output.split("\n"): |
956 | - if not line.strip(): # Skip empty lines |
957 | - continue |
958 | + |
959 | + lines = self._run_command("nova volume-list") |
960 | + for line in lines: |
961 | if "+----" in line or "ID" in line: |
962 | continue |
963 | values = line.split("|") |
964 | @@ -434,15 +440,12 @@ |
965 | Attach a Nova C{volume_id} to the provided C{instance_id} and return |
966 | the device path. |
967 | """ |
968 | - try: |
969 | - device = subprocess.check_output( |
970 | - "nova volume-attach %s %s auto | egrep -o \"/dev/vd[b-z]\"" % |
971 | - (instance_id, volume_id), shell=True) |
972 | - except subprocess.CalledProcessError, e: |
973 | - hookenv.log("ERROR: %s" % str(e), hookenv.ERROR) |
974 | - sys.exit(1) |
975 | - if device.strip(): |
976 | - return device.strip() |
977 | + command = ( |
978 | + "nova volume-attach %s %s auto | egrep -o \"/dev/vd[b-z]\"" % |
979 | + (instance_id, volume_id)) |
980 | + lines = self._run_ec2_command(command) |
981 | + if lines: |
982 | + return lines[0] |
983 | return "" |
984 | |
985 | def _nova_create_volume(self, size, volume_label, instance_id): |
986 | @@ -450,14 +453,11 @@ |
987 | hookenv.log( |
988 | "Creating a %sGig volume named (%s) for instance %s" % |
989 | (size, volume_label, instance_id)) |
990 | - try: |
991 | - subprocess.check_call( |
992 | - "nova volume-create --display-name '%s' %s" % |
993 | - (volume_label, size), shell=True) |
994 | - except subprocess.CalledProcessError, e: |
995 | - hookenv.log("ERROR: %s" % str(e), hookenv.ERROR) |
996 | - sys.exit(1) |
997 | + command = ( |
998 | + "nova volume-create --display-name '%s' %s" % |
999 | + (volume_label, size)) |
1000 | |
1001 | + self._run_command(command) |
1002 | volume_id = self.get_volume_id(volume_label) |
1003 | if not volume_id: |
1004 | hookenv.log( |
1005 | @@ -467,6 +467,102 @@ |
1006 | return volume_id |
1007 | |
1008 | |
1009 | +def parse_ec2_tag_response(line): |
1010 | + """ |
1011 | + Parses a line of output of response type TAG from euca commands and |
1012 | + returns a C{dict} of the values. If not a TAG response, an empty C{dict} is |
1013 | + returned. |
1014 | + """ |
1015 | + values = line.split("\t") |
1016 | + response_type = values[0] |
1017 | + if response_type != "TAG": |
1018 | + return {} |
1019 | + |
1020 | + # There are two versions of the TAG response |
1021 | + if values[1] == "volume": |
1022 | + tag_type = values[1] |
1023 | + object_id = values[2] |
1024 | + else: |
1025 | + tag_type = values[2] |
1026 | + object_id = values[1] |
1027 | + if tag_type != "volume": |
1028 | + hookenv.log( |
1029 | + "Ignoring non-volume TAG response: %s." % line, hookenv.WARNING) |
1030 | + return {} |
1031 | + return { |
1032 | + "tag_type": tag_type, "id": object_id, "key": values[3], |
1033 | + "value": values[4]} |
1034 | + |
1035 | + |
1036 | +def parse_ec2_attachment_response(line): |
1037 | + """ |
1038 | + Parses a line of output of response type ATTACHMENT from euca commands and |
1039 | + returns a C{dict} of the values. If not a ATTACHMENT response, an empty |
1040 | + C{dict} is returned. |
1041 | + """ |
1042 | + values = line.split("\t") |
1043 | + response_type = values[0] |
1044 | + if response_type != "ATTACHMENT": |
1045 | + return {} |
1046 | + |
1047 | + return { |
1048 | + "volume_id": values[1], "instance_id": values[2], "device": values[3], |
1049 | + "attach_status": values[4]} |
1050 | + |
1051 | + |
1052 | +def parse_ec2_volume_response(line): |
1053 | + """ |
1054 | + Parses a line of output of response type VOLUME from euca commands and |
1055 | + returns a C{dict} of the values. If not a VOLUME response, an empty C{dict} |
1056 | + is returned. |
1057 | + """ |
1058 | + values = line.split("\t") |
1059 | + response_type = values[0] |
1060 | + if response_type != "VOLUME": |
1061 | + return {} |
1062 | + |
1063 | + return { |
1064 | + "id": values[1], "size": values[2].strip(), |
1065 | + "snapshot_id": values[3], "availability_zone": values[4], |
1066 | + "status": values[5]} |
1067 | + |
1068 | + |
1069 | +def parse_ec2_instance_response(line): |
1070 | + """ |
1071 | + Parses a line of output of response type INSTANCE from euca commands and |
1072 | + returns a C{dict} of the values. If not an INSTANCE response, and empty |
1073 | + C{dict} is returned. |
1074 | + """ |
1075 | + values = line.split("\t") |
1076 | + response_type = values[0] |
1077 | + if response_type != "INSTANCE": |
1078 | + return {} |
1079 | + |
1080 | + if len(values) not in [EUCA2_INSTANCE_RESPONSE_LENGTH_V2, |
1081 | + EUCA2_INSTANCE_RESPONSE_LENGTH_V3]: |
1082 | + hookenv.log( |
1083 | + "ERROR: Unsupported euca INSTANCE output format version", |
1084 | + hookenv.ERROR) |
1085 | + sys.exit(1) |
1086 | + |
1087 | + (ec2_instance_id, image_id) = values[1:3] |
1088 | + public_dns = values[3] |
1089 | + private_dns = values[4] |
1090 | + state = values[5] |
1091 | + instance_type = values[9] |
1092 | + availability_zone = values[11] |
1093 | + kernel = values[12] |
1094 | + public_ip = values[16] |
1095 | + private_ip = values[17] |
1096 | + |
1097 | + return { |
1098 | + "ip-address": public_ip, "private-ip-address": private_ip, |
1099 | + "image-id": image_id, "instance-type": instance_type, |
1100 | + "kernel": kernel, "private-dns-name": private_dns, |
1101 | + "public-dns-name": public_dns, "state": state, |
1102 | + "availability_zone": availability_zone, "instance_id": ec2_instance_id} |
1103 | + |
1104 | + |
1105 | def generate_volume_label(remote_unit): |
1106 | """Create a volume label for the requesting remote unit""" |
1107 | return "%s unit volume" % remote_unit |
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:
options:
provider: block-storage- broker
volume_ size: 9
block- storage- broker- ec2:
options:
provider: ec2
key: <YOUR_EC2_ ACCESS_ KEY>
endpoint: <YOUR_EC2_URL>
secret: <YOUR_EC2_ SECRET_ KEY>
constraint s: mem=2048
options:
extra- packages: python-apt postgresql-contrib postgresql- 9.3-debversion
max_connectio ns: 500
services:
storage:
branch: lp:~chad.smith/charms/precise/storage/storage-volume-label-availability-zone
branch: lp:~chad.smith/charms/precise/block-storage-broker/bsb-trusty-support
postgresql:
branch: lp:~chad.smith/charms/precise/postgresql/postgresql-using-storage-subordinate
doit: broker- ec2]
inherits: common
series: trusty
relations:
- [postgresql, storage]
- [storage, block-storage-
juju-deployer -c block-storage- trusty. yaml doit