1
import nova_util as util
5
from testing import TestHookenv
8
class TestNovaUtil(mocker.MockerTestCase):
12
util.hookenv = TestHookenv(
13
{"key": "myusername", "tenant": "myusername_project",
14
"secret": "password", "region": "region1",
15
"endpoint": "https://keystone_url:443/v2.0/",
16
"default_volume_size": 11})
17
util.log = util.hookenv.log
19
def test_load_environment_with_nova_variables(self):
21
L{load_environment} will setup script environment variables for nova
22
by mapping configuration values provided to openstack OS_* environment
23
variables and then call L{validate_credentials} to assert
24
that environment variables provided give access to the service.
26
self.addCleanup(setattr, util.os, "environ", util.os.environ)
28
credentials = self.mocker.replace(util.validate_credentials)
32
util.load_environment()
34
"OS_AUTH_URL": "https://keystone_url:443/v2.0/",
35
"OS_PASSWORD": "password",
36
"OS_REGION_NAME": "region1",
37
"OS_TENANT_NAME": "myusername_project",
38
"OS_USERNAME": "myusername"
40
self.assertEqual(util.os.environ, expected)
42
def test_load_environment_error_missing_config_options(self):
44
L{load_environment} will exit in failure and log a message if any
45
required configuration option is not set.
47
self.addCleanup(setattr, util.os, "environ", util.os.environ)
48
credentials = self.mocker.replace(util.validate_credentials)
50
self.mocker.throw(SystemExit)
53
self.assertRaises(SystemExit, util.load_environment)
55
def test_validate_credentials_failure(self):
57
L{validate_credentials} will attempt a simple nova command to ensure
58
the environment is properly configured to access the nova service.
59
Upon failure to contact the nova service, L{validate_credentials} will
60
exit in error and log a message.
63
nova_cmd = self.mocker.replace(subprocess.check_call)
64
nova_cmd(command, shell=True)
65
self.mocker.throw(subprocess.CalledProcessError(1, command))
68
result = self.assertRaises(SystemExit, util.validate_credentials)
69
self.assertEqual(result.code, 1)
71
"ERROR: Charm configured credentials can't access endpoint. "
72
"Command '%s' returned non-zero exit status 1" % command)
74
message, util.hookenv._log_ERROR, "Not logged- %s" % message)
76
def test_validate_credentials(self):
78
L{validate_credentials} will succeed when a simple nova command
79
succeeds due to a properly configured environment based on the charm
80
configuration options.
83
nova_cmd = self.mocker.replace(subprocess.check_call)
84
nova_cmd(command, shell=True)
87
util.validate_credentials()
89
"Validated charm configuration credentials have access to "
90
"block storage service"
93
message, util.hookenv._log_INFO, "Not logged- %s" % message)
95
def test_get_volume_attachments_present(self):
97
L{get_volume_attachments} returns a C{list} of available volume
98
attachments for the given C{volume_id}.
100
volume_id = "123-123-123"
102
"nova volume-show %s | grep attachments | awk -F '|' '{print $3}'"
104
nova_cmd = self.mocker.replace(subprocess.check_output)
105
nova_cmd(command, shell=True)
107
"[{u'device': u'/dev/vdc', u'server_id': u'blah', "
108
"u'id': u'i-123123', u'volume_id': u'%s'}]" % volume_id)
112
"device": "/dev/vdc", "server_id": "blah", "id": "i-123123",
113
"volume_id": volume_id}]
115
self.assertEqual(util.get_volume_attachments(volume_id), expected)
117
def test_get_volume_attachments_no_attachments_present(self):
119
L{get_volume_attachments} returns an empty C{list} if no available
120
volume attachments are reported for the given C{volume_id}.
122
volume_id = "123-123-123"
124
"nova volume-show %s | grep attachments | awk -F '|' '{print $3}'"
126
nova_cmd = self.mocker.replace(subprocess.check_output)
127
nova_cmd(command, shell=True)
128
self.mocker.result("[]")
131
self.assertEqual(util.get_volume_attachments(volume_id), [])
133
def test_get_volume_attachments_no_volume_present(self):
135
L{get_volume_attachments} returns an empty C{list} if no available
136
volume is discovered for the given C{volume_id}.
138
volume_id = "123-123-123"
140
"nova volume-show %s | grep attachments | awk -F '|' '{print $3}'"
142
nova_cmd = self.mocker.replace(subprocess.check_output)
143
nova_cmd(command, shell=True)
144
self.mocker.throw(subprocess.CalledProcessError(1, command))
147
self.assertEqual(util.get_volume_attachments(volume_id), [])
149
def test_volume_exists_true(self):
151
L{volume_exists} returns C{True} when C{volume_id} is seen by the nova
152
client command C{nova volume-show}.
154
volume_id = "123134124-1241412-1242141"
155
command = "nova volume-show %s" % volume_id
156
nova_cmd = self.mocker.replace(subprocess.call)
157
nova_cmd(command, shell=True)
158
self.mocker.result(0)
160
self.assertTrue(util.volume_exists(volume_id))
162
def test_volume_exists_false(self):
164
L{volume_exists} returns C{False} when C{volume_id} is not seen by the
165
nova client command C{nova volume-show}.
167
volume_id = "123134124-1241412-1242141"
168
command = "nova volume-show %s" % volume_id
169
nova_cmd = self.mocker.replace(subprocess.call)
170
nova_cmd(command, shell=True)
171
self.mocker.throw(subprocess.CalledProcessError(1, "Volume not here"))
174
self.assertFalse(util.volume_exists(volume_id))
176
def test_get_volume_id_by_volume_name(self):
178
L{get_volume_id} provided with a existing C{volume_name} returns the
179
corresponding nova volume id.
181
volume_name = "my-volume"
182
volume_id = "12312412-412312\n"
184
"nova volume-show '%s' | grep ' id ' | awk '{ print $4 }'" %
186
nova_cmd = self.mocker.replace(subprocess.check_output)
187
nova_cmd(command, shell=True)
188
self.mocker.result(volume_id)
190
self.assertEqual(util.get_volume_id(volume_name), volume_id.strip())
192
def test_get_volume_id_command_error(self):
194
L{get_volume_id} handles any nova command error by reporting the error
195
and exiting the hook.
197
volume_name = "my-volume"
199
"nova volume-show '%s' | grep ' id ' | awk '{ print $4 }'" %
201
nova_cmd = self.mocker.replace(subprocess.check_output)
202
nova_cmd(command, shell=True)
203
self.mocker.throw(subprocess.CalledProcessError(1, command))
206
result = self.assertRaises(SystemExit, util.get_volume_id, volume_name)
207
self.assertEqual(result.code, 1)
209
"ERROR: Couldn't find nova volume id for %s. Command '%s' "
210
"returned non-zero exit status 1" % (volume_name, command))
212
message, util.hookenv._log_ERROR, "Not logged- %s" % message)
214
def test_get_volume_id_without_volume_name(self):
216
L{get_volume_id} without a provided C{volume_name} will discover the
217
nova volume id by searching nova volume-list for volumes labelled with
218
the os.environ[JUJU_REMOTE_UNIT].
220
unit_name = "postgresql/0"
222
setattr, os, "environ", os.environ)
223
os.environ = {"JUJU_REMOTE_UNIT": unit_name}
224
volume_id = "123134124-1241412-1242141\n"
226
"nova volume-list | grep %s | awk '{print $2}'" % unit_name)
227
nova_cmd = self.mocker.replace(subprocess.check_output)
228
nova_cmd(command, shell=True)
229
self.mocker.result(volume_id)
232
self.assertEqual(util.get_volume_id(), volume_id.strip())
234
def test_get_volume_id_without_volume_name_no_matching_volume(self):
236
L{get_volume_id} without a provided C{volume_name} will return C{None}
237
when it cannot find a matching volume label from nova volume-list for
238
the os.environ[JUJU_REMOTE_UNIT].
240
unit_name = "postgresql/0"
242
setattr, os, "environ", os.environ)
243
os.environ = {"JUJU_REMOTE_UNIT": unit_name}
245
"nova volume-list | grep %s | awk '{print $2}'" % unit_name)
246
nova_cmd = self.mocker.replace(subprocess.check_output)
247
nova_cmd(command, shell=True)
248
self.mocker.result("\n") # Empty result string from awk
251
self.assertIsNone(util.get_volume_id())
253
def test_get_volume_id_without_volume_name_multiple_matching_volumes(self):
255
L{get_volume_id} does not support multiple volumes associated with the
256
the instance represented by os.environ[JUJU_REMOTE_UNIT]. When
257
C{volume_name} is not specified and nova volume-list returns multiple
258
results the function exits with an error.
260
unit_name = "postgresql/0"
261
self.addCleanup(setattr, os, "environ", os.environ)
262
os.environ = {"JUJU_REMOTE_UNIT": unit_name}
264
"nova volume-list | grep %s | awk '{print $2}'" % unit_name)
265
nova_cmd = self.mocker.replace(subprocess.check_output)
266
nova_cmd(command, shell=True)
267
self.mocker.result("123-123-123\n456-456-456\n") # Two results
270
result = self.assertRaises(SystemExit, util.get_volume_id)
271
self.assertEqual(result.code, 1)
273
"Error: Multiple nova volumes labeled as associated with "
274
"%s. Cannot get_volume_id." % unit_name)
276
message, util.hookenv._log_ERROR, "Not logged- %s" % message)
278
def test_get_volume_status_by_known_volume_id(self):
280
L{get_volume_status} returns the status of a volume matching
281
C{volume_id} by using the nova client commands.
283
volume_id = "123134124-1241412-1242141"
285
"nova volume-show '%s' | grep ' status ' | awk '{ print $4 }'" %
287
nova_cmd = self.mocker.replace(subprocess.check_output)
288
nova_cmd(command, shell=True)
289
self.mocker.result("available\n")
291
self.assertEqual(util.get_volume_status(volume_id), "available")
293
def test_get_volume_status_by_invalid_volume_id(self):
295
L{get_volume_status} returns the status of a volume matching
296
C{volume_id} by using the nova client commands.
298
volume_id = "123134124-1241412-1242141"
300
"nova volume-show '%s' | grep ' status ' | awk '{ print $4 }'" %
302
nova_cmd = self.mocker.replace(subprocess.check_output)
303
nova_cmd(command, shell=True)
304
self.mocker.throw(subprocess.CalledProcessError(1, command))
306
self.assertIsNone(util.get_volume_status(volume_id))
308
"Error: nova couldn't get status of volume %s. "
309
"Command '%s' returned non-zero exit status 1" %
310
(volume_id, command))
312
message, util.hookenv._log_ERROR, "Not logged- %s" % message)
314
def test_get_volume_status_when_get_volume_id_none(self):
316
L{get_volume_status} logs a warning and returns C{None} when
317
C{volume_id} is not specified and L{get_volume_id} returns C{None}.
319
get_vol_id = self.mocker.replace(util.get_volume_id)
321
self.mocker.result(None)
324
self.assertIsNone(util.get_volume_status())
325
message = "WARNING: Can't find volume_id to get status."
327
message, util.hookenv._log_WARNING, "Not logged- %s" % message)
329
def test_get_volume_status_when_get_volume_id_discovers(self):
331
When C{volume_id} is not specified, L{get_volume_status} obtains the
332
volume id from L{get_volume_id} gets the status using nova commands.
334
volume_id = "123-123-123"
335
get_vol_id = self.mocker.replace(util.get_volume_id)
337
self.mocker.result(volume_id)
339
"nova volume-show '%s' | grep ' status ' | awk '{ print $4 }'" %
341
nova_cmd = self.mocker.replace(subprocess.check_output)
342
nova_cmd(command, shell=True)
343
self.mocker.result("in-use\n")
346
self.assertEqual(util.get_volume_status(), "in-use")
348
def test_attach_nova_volume_failure_when_volume_id_does_not_exist(self):
350
When L{attach_nova_volume} is provided a C{volume_id} that doesn't
351
exist it logs and error and exits.
353
unit_name = "postgresql/0"
354
instance_id = "i-123123"
355
volume_id = "123-123-123"
356
self.addCleanup(setattr, os, "environ", os.environ)
357
os.environ = {"JUJU_REMOTE_UNIT": unit_name}
359
load_environment = self.mocker.replace(util.load_environment)
361
volume_exists = self.mocker.replace(util.volume_exists)
362
volume_exists(volume_id)
363
self.mocker.result(False)
366
result = self.assertRaises(
367
SystemExit, util.attach_nova_volume, instance_id, volume_id)
368
self.assertEqual(result.code, 1)
370
"Requested volume-id (%s) does not exist. "
371
"Unable to associate storage with %s" % (volume_id, unit_name))
373
message, util.hookenv._log_ERROR, "Not logged- %s" % message)
375
def test_attach_nova_volume_when_volume_id_already_attached(self):
377
When L{attach_nova_volume} is provided a C{volume_id} that already
378
has the state C{in-use} it logs that the volume is already attached
381
unit_name = "postgresql/0"
382
instance_id = "i-123123"
383
volume_id = "123-123-123"
384
self.addCleanup(setattr, os, "environ", os.environ)
385
os.environ = {"JUJU_REMOTE_UNIT": unit_name}
387
load_environment = self.mocker.replace(util.load_environment)
389
volume_exists = self.mocker.replace(util.volume_exists)
390
volume_exists(volume_id)
391
self.mocker.result(True)
392
get_vol_status = self.mocker.replace(util.get_volume_status)
393
get_vol_status(volume_id)
394
self.mocker.result("in-use")
395
get_attachments = self.mocker.replace(util.get_volume_attachments)
396
get_attachments(volume_id)
397
self.mocker.result([{"device": "/dev/vdc"}])
401
util.attach_nova_volume(instance_id, volume_id), "/dev/vdc")
403
message = "Volume %s already attached. Done" % volume_id
405
message, util.hookenv._log_INFO, "Not logged- %s" % message)
407
def test_attach_nova_volume_failure_when_volume_unsupported_status(self):
409
When L{attach_nova_volume} is provided a C{volume_id} that has an
410
unsupported status. It logs the error and exits.
412
unit_name = "postgresql/0"
413
instance_id = "i-123123"
414
volume_id = "123-123-123"
415
self.addCleanup(setattr, os, "environ", os.environ)
416
os.environ = {"JUJU_REMOTE_UNIT": unit_name}
418
load_environment = self.mocker.replace(util.load_environment)
420
volume_exists = self.mocker.replace(util.volume_exists)
421
volume_exists(volume_id)
422
self.mocker.result(True)
423
get_vol_status = self.mocker.replace(util.get_volume_status)
424
get_vol_status(volume_id)
425
self.mocker.result("deleting")
428
result = self.assertRaises(
429
SystemExit, util.attach_nova_volume, instance_id, volume_id)
430
self.assertEqual(result.code, 1)
431
message = ("Cannot attach nova volume. "
432
"Volume has unsupported status: deleting")
434
message, util.hookenv._log_INFO, "Not logged- %s" % message)
436
def test_attach_nova_volume_creates_with_config_size(self):
438
When C{volume_id} is C{None}, L{attach_nova_volume} will create a new
439
nova volume with the configured C{default_volume_size} when the volume
440
doesn't exist and C{size} is not provided.
442
unit_name = "postgresql/0"
443
instance_id = "i-123123"
444
volume_id = "123-123-123"
445
volume_label = "%s unit volume" % unit_name
446
default_volume_size = util.hookenv.config("default_volume_size")
447
self.addCleanup(setattr, os, "environ", os.environ)
448
os.environ = {"JUJU_REMOTE_UNIT": unit_name}
450
load_environment = self.mocker.replace(util.load_environment)
452
get_vol_id = self.mocker.replace(util.get_volume_id)
453
get_vol_id(volume_label)
454
self.mocker.result(None)
456
"nova volume-create --display-name '%s' %s" %
457
(volume_label, default_volume_size))
458
nova_cmd = self.mocker.replace(subprocess.check_call)
459
nova_cmd(command, shell=True)
460
get_vol_id(volume_label)
461
self.mocker.result(volume_id) # Found the volume now
462
get_vol_status = self.mocker.replace(util.get_volume_status)
463
get_vol_status(volume_id)
464
self.mocker.result("available")
466
"nova volume-attach %s %s auto | egrep -o \"/dev/vd[b-z]\"" %
467
(instance_id, volume_id))
468
attach_cmd = self.mocker.replace(subprocess.check_output)
469
attach_cmd(command, shell=True)
470
self.mocker.result("/dev/vdc\n")
473
self.assertEqual(util.attach_nova_volume(instance_id), "/dev/vdc")
475
"Creating a %sGig volume named (%s) for instance %s" %
476
(default_volume_size, volume_label, instance_id),
477
"Attaching %s (%s)" % (volume_label, volume_id)]
478
for message in messages:
480
message, util.hookenv._log_INFO, "Not logged- %s" % message)
482
def test_detach_nova_volume_no_volume_found(self):
484
When L{get_volume_id} is unable to find an attached volume and returns
485
C{None}, L{detach_volume} will log a message and perform no work.
487
instance_id = "i-123123"
488
load_environment = self.mocker.replace(util.load_environment)
490
get_vol_id = self.mocker.replace(util.get_volume_id)
491
get_vol_id(instance_id=instance_id)
492
self.mocker.result(None)
495
util.detach_nova_volume(instance_id)
496
message = "Cannot find volume name to detach, done"
498
message, util.hookenv._log_INFO, "Not logged- %s" % message)
500
def test_detach_nova_volume_volume_already_detached(self):
502
When L{get_volume_id} finds a volume that is already C{available} it
503
logs that the volume is already detached and does no work.
505
instance_id = "i-123123"
506
volume_id = "123-123-123"
507
load_environment = self.mocker.replace(util.load_environment)
509
get_vol_id = self.mocker.replace(util.get_volume_id)
510
get_vol_id(instance_id=instance_id)
511
self.mocker.result(volume_id)
512
get_vol_status = self.mocker.replace(util.get_volume_status)
513
get_vol_status(volume_id)
514
self.mocker.result("available")
517
util.detach_nova_volume(instance_id) # pass in our instance_id
518
message = "Volume (%s) already detached. Done" % volume_id
520
message, util.hookenv._log_INFO, "Not logged- %s" % message)
522
def test_detach_nova_volume_command_error(self):
524
When the nova volume-detach command fails, L{detach_nova_volume} will
525
log a message and exit in error.
527
volume_id = "123-123-123"
528
instance_id = "i-123123"
529
load_environment = self.mocker.replace(util.load_environment)
531
get_vol_id = self.mocker.replace(util.get_volume_id)
532
get_vol_id(instance_id=instance_id)
533
self.mocker.result(volume_id)
534
get_vol_status = self.mocker.replace(util.get_volume_status)
535
get_vol_status(volume_id)
536
self.mocker.result("in-use")
537
command = "nova volume-detach %s %s" % (instance_id, volume_id)
538
nova_cmd = self.mocker.replace(subprocess.check_call)
539
nova_cmd(command, shell=True)
540
self.mocker.throw(subprocess.CalledProcessError(1, command))
543
result = self.assertRaises(
544
SystemExit, util.detach_nova_volume, instance_id)
545
self.assertEqual(result.code, 1)
547
"ERROR: Couldn't detach nova volume %s. Command '%s' "
548
"returned non-zero exit status 1" % (volume_id, command))
550
message, util.hookenv._log_ERROR, "Not logged- %s" % message)
552
def test_detach_nova_volume(self):
554
When L{get_volume_id} finds a volume associated with this instance
555
which has a volume state not equal to C{available}, it detaches that
556
volume using nova commands.
558
volume_id = "123-123-123"
559
instance_id = "i-123123"
560
load_environment = self.mocker.replace(util.load_environment)
562
get_vol_id = self.mocker.replace(util.get_volume_id)
563
get_vol_id(instance_id=instance_id)
564
self.mocker.result(volume_id)
565
get_vol_status = self.mocker.replace(util.get_volume_status)
566
get_vol_status(volume_id)
567
self.mocker.result("in-use")
568
command = "nova volume-detach %s %s" % (instance_id, volume_id)
569
nova_cmd = self.mocker.replace(subprocess.check_call)
570
nova_cmd(command, shell=True)
573
util.detach_nova_volume(instance_id)
575
"Detaching volume (%s) from instance %s" %
576
(volume_id, instance_id))
578
message, util.hookenv._log_INFO, "Not logged- %s" % message)