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