Skip to content

Commit 2be3de2

Browse files
Jenkinsopenstack-gerrit
authored andcommitted
Merge "Make block-device-mapping more stable and clear"
2 parents 411cda7 + 7a7bb06 commit 2be3de2

6 files changed

Lines changed: 519 additions & 42 deletions

File tree

doc/source/command-objects/server.rst

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,23 @@ Create a new server
193193

194194
.. option:: --block-device-mapping <dev-name=mapping>
195195

196-
Map block devices; map is <id>:<type>:<size(GB)>:<delete_on_terminate> (optional extension)
196+
Create a block device on the server.
197+
198+
Block device mapping in the format
199+
200+
<dev-name>=<id>:<type>:<size(GB)>:<delete-on-terminate>
201+
202+
<dev-name>: block device name, like: vdb, xvdc (required)
203+
204+
<id>: UUID of the volume or snapshot (required)
205+
206+
<type>: volume or snapshot; default: volume (optional)
207+
208+
<size(GB)>: volume size if create from snapshot (optional)
209+
210+
<delete-on-terminate>: true or false; default: false (optional)
211+
212+
(optional extension)
197213

198214
.. option:: --nic <net-id=net-uuid,v4-fixed-ip=ip-addr,v6-fixed-ip=ip-addr,port-id=port-uuid,auto,none>
199215

openstackclient/compute/v2/server.py

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -446,10 +446,22 @@ def get_parser(self, prog_name):
446446
parser.add_argument(
447447
'--block-device-mapping',
448448
metavar='<dev-name=mapping>',
449-
action='append',
450-
default=[],
451-
help=_('Map block devices; map is '
452-
'<id>:<type>:<size(GB)>:<delete_on_terminate> '
449+
action=parseractions.KeyValueAction,
450+
default={},
451+
# NOTE(RuiChen): Add '\n' at the end of line to put each item in
452+
# the separated line, avoid the help message looks
453+
# messy, see _SmartHelpFormatter in cliff.
454+
help=_('Create a block device on the server.\n'
455+
'Block device mapping in the format\n'
456+
'<dev-name>=<id>:<type>:<size(GB)>:<delete-on-terminate>\n'
457+
'<dev-name>: block device name, like: vdb, xvdc '
458+
'(required)\n'
459+
'<id>: UUID of the volume or snapshot (required)\n'
460+
'<type>: volume or snapshot; default: volume (optional)\n'
461+
'<size(GB)>: volume size if create from snapshot '
462+
'(optional)\n'
463+
'<delete-on-terminate>: true or false; default: false '
464+
'(optional)\n'
453465
'(optional extension)'),
454466
)
455467
parser.add_argument(
@@ -593,33 +605,39 @@ def take_action(self, parsed_args):
593605
'source_type': 'volume',
594606
'destination_type': 'volume'
595607
}]
596-
for dev_map in parsed_args.block_device_mapping:
597-
dev_name, dev_map = dev_map.split('=', 1)
598-
if dev_map:
599-
dev_map = dev_map.split(':')
600-
if len(dev_map) > 0:
601-
mapping = {
602-
'device_name': dev_name,
603-
'uuid': utils.find_resource(
604-
volume_client.volumes,
605-
dev_map[0],
606-
).id}
607-
# Block device mapping v1 compatibility
608-
if len(dev_map) > 1 and \
609-
dev_map[1] in ('volume', 'snapshot'):
610-
mapping['source_type'] = dev_map[1]
611-
else:
612-
mapping['source_type'] = 'volume'
613-
mapping['destination_type'] = 'volume'
614-
if len(dev_map) > 2 and dev_map[2]:
615-
mapping['volume_size'] = dev_map[2]
616-
if len(dev_map) > 3:
617-
mapping['delete_on_termination'] = dev_map[3]
608+
# Handle block device by device name order, like: vdb -> vdc -> vdd
609+
for dev_name in sorted(six.iterkeys(parsed_args.block_device_mapping)):
610+
dev_map = parsed_args.block_device_mapping[dev_name]
611+
dev_map = dev_map.split(':')
612+
if dev_map[0]:
613+
mapping = {'device_name': dev_name}
614+
# 1. decide source and destination type
615+
if (len(dev_map) > 1 and
616+
dev_map[1] in ('volume', 'snapshot')):
617+
mapping['source_type'] = dev_map[1]
618618
else:
619-
msg = _("Volume name or ID must be specified if "
620-
"--block-device-mapping is specified")
621-
raise exceptions.CommandError(msg)
622-
block_device_mapping_v2.append(mapping)
619+
mapping['source_type'] = 'volume'
620+
mapping['destination_type'] = 'volume'
621+
# 2. check target exist, update target uuid according by
622+
# source type
623+
if mapping['source_type'] == 'volume':
624+
volume_id = utils.find_resource(
625+
volume_client.volumes, dev_map[0]).id
626+
mapping['uuid'] = volume_id
627+
elif mapping['source_type'] == 'snapshot':
628+
snapshot_id = utils.find_resource(
629+
volume_client.volume_snapshots, dev_map[0]).id
630+
mapping['uuid'] = snapshot_id
631+
# 3. append size and delete_on_termination if exist
632+
if len(dev_map) > 2 and dev_map[2]:
633+
mapping['volume_size'] = dev_map[2]
634+
if len(dev_map) > 3 and dev_map[3]:
635+
mapping['delete_on_termination'] = dev_map[3]
636+
else:
637+
msg = _("Volume or snapshot (name or ID) must be specified if "
638+
"--block-device-mapping is specified")
639+
raise exceptions.CommandError(msg)
640+
block_device_mapping_v2.append(mapping)
623641

624642
nics = []
625643
auto_or_none = False

openstackclient/tests/functional/compute/v2/test_server.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,113 @@ def test_server_boot_from_volume(self):
388388
cmd_output['status'],
389389
)
390390

391+
def test_server_boot_with_bdm_snapshot(self):
392+
"""Test server create from image with bdm snapshot, server delete"""
393+
# get volume status wait function
394+
volume_wait_for = test_volume.VolumeTests(
395+
methodName='wait_for',
396+
).wait_for
397+
398+
# create source empty volume
399+
empty_volume_name = uuid.uuid4().hex
400+
cmd_output = json.loads(self.openstack(
401+
'volume create -f json ' +
402+
'--size 1 ' +
403+
empty_volume_name
404+
))
405+
self.assertIsNotNone(cmd_output["id"])
406+
self.addCleanup(self.openstack,
407+
'volume delete ' + empty_volume_name)
408+
self.assertEqual(
409+
empty_volume_name,
410+
cmd_output['name'],
411+
)
412+
volume_wait_for("volume", empty_volume_name, "available")
413+
414+
# create snapshot of source empty volume
415+
empty_snapshot_name = uuid.uuid4().hex
416+
cmd_output = json.loads(self.openstack(
417+
'volume snapshot create -f json ' +
418+
'--volume ' + empty_volume_name + ' ' +
419+
empty_snapshot_name
420+
))
421+
self.assertIsNotNone(cmd_output["id"])
422+
self.assertEqual(
423+
empty_snapshot_name,
424+
cmd_output['name'],
425+
)
426+
volume_wait_for("volume snapshot", empty_snapshot_name, "available")
427+
428+
# create server with bdm snapshot
429+
server_name = uuid.uuid4().hex
430+
server = json.loads(self.openstack(
431+
'server create -f json ' +
432+
'--flavor ' + self.flavor_name + ' ' +
433+
'--image ' + self.image_name + ' ' +
434+
'--block-device-mapping '
435+
'vdb=' + empty_snapshot_name + ':snapshot:1:true ' +
436+
self.network_arg + ' ' +
437+
'--wait ' +
438+
server_name
439+
))
440+
self.assertIsNotNone(server["id"])
441+
self.assertEqual(
442+
server_name,
443+
server['name'],
444+
)
445+
self.wait_for_status(server_name, 'ACTIVE')
446+
447+
# check server volumes_attached, format is
448+
# {"volumes_attached": "id='2518bc76-bf0b-476e-ad6b-571973745bb5'",}
449+
cmd_output = json.loads(self.openstack(
450+
'server show -f json ' +
451+
server_name
452+
))
453+
volumes_attached = cmd_output['volumes_attached']
454+
self.assertTrue(volumes_attached.startswith('id='))
455+
attached_volume_id = volumes_attached.replace('id=', '')
456+
457+
# check the volume that attached on server
458+
cmd_output = json.loads(self.openstack(
459+
'volume show -f json ' +
460+
attached_volume_id
461+
))
462+
attachments = cmd_output['attachments']
463+
self.assertEqual(
464+
1,
465+
len(attachments),
466+
)
467+
self.assertEqual(
468+
server['id'],
469+
attachments[0]['server_id'],
470+
)
471+
self.assertEqual(
472+
"in-use",
473+
cmd_output['status'],
474+
)
475+
476+
# delete server, then check the attached volume had been deleted,
477+
# <delete-on-terminate>=true
478+
self.openstack('server delete --wait ' + server_name)
479+
cmd_output = json.loads(self.openstack(
480+
'volume list -f json'
481+
))
482+
target_volume = [each_volume
483+
for each_volume in cmd_output
484+
if each_volume['ID'] == attached_volume_id]
485+
if target_volume:
486+
# check the attached volume is 'deleting' status
487+
self.assertEqual('deleting', target_volume[0]['Status'])
488+
else:
489+
# the attached volume had been deleted
490+
pass
491+
492+
# clean up volume snapshot manually, make sure the snapshot and volume
493+
# can be deleted sequentially, self.addCleanup so fast, that cause
494+
# volume service API 400 error and the volume is left over at the end.
495+
self.openstack('volume snapshot delete ' + empty_snapshot_name)
496+
volume_wait_for('volume snapshot', empty_snapshot_name, 'disappear')
497+
391498
def test_server_create_with_none_network(self):
392499
"""Test server create with none network option."""
393500
server_name = uuid.uuid4().hex

openstackclient/tests/functional/volume/v2/test_volume.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import time
1515
import uuid
1616

17+
from tempest.lib import exceptions
18+
1719
from openstackclient.tests.functional.volume.v2 import common
1820

1921

@@ -253,7 +255,13 @@ def wait_for(self, check_type, check_name, desired_status, wait=120,
253255
total_sleep = 0
254256
opts = self.get_opts(['status'])
255257
while total_sleep < wait:
256-
status = self.openstack(check_type + ' show ' + check_name + opts)
258+
try:
259+
status = self.openstack(
260+
check_type + ' show ' + check_name + opts
261+
)
262+
except exceptions.CommandFailed:
263+
# Show command raise Exception when object had been deleted
264+
status = 'disappear'
257265
status = status.rstrip()
258266
print('Checking {} {} Waiting for {} current status: {}'
259267
.format(check_type, check_name, desired_status, status))

0 commit comments

Comments
 (0)