New syntax to boot from a block device mapping

Add new arguments and syntax for booting from a block device mapping
that use the new os-block-device-mapping-v2-boot extension. These
allow to:

 * boot from an image, volume or snapshot (--image, --boot-volume, --snapshot)
 * attach any type of block device (--block-device).
 * attach an swap disk on boot (--swap).
 * attach an ephemeral disk on boot (--ephemeral).

blueprint: improve-block-device-handling

DocImpact

Change-Id: I1aadeafed82b3bd1febcf0d1c3e64b258d6abeda
This commit is contained in:
Xavier Queralt 2013-07-26 09:23:19 +02:00
parent d770bb3aab
commit 6a85c954c5
5 changed files with 343 additions and 34 deletions

View File

@ -232,13 +232,45 @@ class ManagerWithFind(Manager):
class BootingManagerWithFind(ManagerWithFind):
"""Like a `ManagerWithFind`, but has the ability to boot servers."""
def _parse_block_device_mapping(self, block_device_mapping):
bdm = []
for device_name, mapping in block_device_mapping.iteritems():
#
# The mapping is in the format:
# <id>:[<type>]:[<size(GB)>]:[<delete_on_terminate>]
#
bdm_dict = {'device_name': device_name}
mapping_parts = mapping.split(':')
source_id = mapping_parts[0]
if len(mapping_parts) == 1:
bdm_dict['volume_id'] = source_id
elif len(mapping_parts) > 1:
source_type = mapping_parts[1]
if source_type.startswith('snap'):
bdm_dict['snapshot_id'] = source_id
else:
bdm_dict['volume_id'] = source_id
if len(mapping_parts) > 2 and mapping_parts[2]:
bdm_dict['volume_size'] = str(int(mapping_parts[2]))
if len(mapping_parts) > 3:
bdm_dict['delete_on_termination'] = mapping_parts[3]
bdm.append(bdm_dict)
return bdm
def _boot(self, resource_url, response_key, name, image, flavor,
meta=None, files=None, userdata=None,
reservation_id=None, return_raw=False, min_count=None,
max_count=None, security_groups=None, key_name=None,
availability_zone=None, block_device_mapping=None, nics=None,
scheduler_hints=None, config_drive=None, admin_pass=None,
disk_config=None, **kwargs):
availability_zone=None, block_device_mapping=None,
block_device_mapping_v2=None, nics=None, scheduler_hints=None,
config_drive=None, admin_pass=None, disk_config=None, **kwargs):
"""
Create (boot) a new server.
@ -263,6 +295,8 @@ class BootingManagerWithFind(ManagerWithFind):
placement.
:param block_device_mapping: A dict of block device mappings for this
server.
:param block_device_mapping_v2: A dict of block device mappings V2 for
this server.
:param nics: (optional extension) an ordered list of nics to be
added to this server, with information about
connected networks, fixed ips, etc.
@ -329,30 +363,10 @@ class BootingManagerWithFind(ManagerWithFind):
# Block device mappings are passed as a list of dictionaries
if block_device_mapping:
bdm = body['server']['block_device_mapping'] = []
for device_name, mapping in block_device_mapping.items():
#
# The mapping is in the format:
# <id>:[<type>]:[<size(GB)>]:[<delete_on_terminate>]
#
bdm_dict = {'device_name': device_name}
mapping_parts = mapping.split(':')
id = mapping_parts[0]
if len(mapping_parts) == 1:
bdm_dict['volume_id'] = id
if len(mapping_parts) > 1:
type = mapping_parts[1]
if type.startswith('snap'):
bdm_dict['snapshot_id'] = id
else:
bdm_dict['volume_id'] = id
if len(mapping_parts) > 2:
if mapping_parts[2]:
bdm_dict['volume_size'] = str(int(mapping_parts[2]))
if len(mapping_parts) > 3:
bdm_dict['delete_on_termination'] = mapping_parts[3]
bdm.append(bdm_dict)
body['server']['block_device_mapping'] = \
self._parse_block_device_mapping(block_device_mapping)
elif block_device_mapping_v2:
body['server']['block_device_mapping_v2'] = block_device_mapping_v2
if nics is not None:
# NOTE(tr3buchet): nics can be an empty list

View File

@ -364,8 +364,18 @@ class FakeHTTPClient(base_client.HTTPClient):
def post_os_volumes_boot(self, body, **kw):
assert set(body.keys()) <= set(['server', 'os:scheduler_hints'])
fakes.assert_has_keys(body['server'],
required=['name', 'block_device_mapping', 'flavorRef'],
required=['name', 'flavorRef'],
optional=['imageRef'])
# Require one, and only one, of the keys for bdm
if 'block_device_mapping' not in body['server']:
if 'block_device_mapping_v2' not in body['server']:
raise AssertionError(
"missing required keys: 'block_device_mapping'"
)
elif 'block_device_mapping_v2' in body['server']:
raise AssertionError("found extra keys: 'block_device_mapping'")
return (202, {}, self.get_servers_9012()[2])
def get_servers_1234(self, **kw):

View File

@ -289,6 +289,163 @@ class ShellTest(utils.TestCase):
}},
)
def test_boot_image_bdms_v2(self):
self.run_command(
'boot --flavor 1 --image 1 --block-device id=fake-id,'
'source=volume,dest=volume,device=vda,size=1,format=ext4,'
'type=disk,shutdown=preserve some-server'
)
self.assert_called_anytime(
'POST', '/os-volumes_boot',
{'server': {
'flavorRef': '1',
'name': 'some-server',
'block_device_mapping_v2': [
{
'uuid': 1,
'source_type': 'image',
'destination_type': 'local',
'boot_index': 0,
'delete_on_termination': True,
},
{
'uuid': 'fake-id',
'source_type': 'volume',
'destination_type': 'volume',
'device_name': 'vda',
'volume_size': '1',
'guest_format': 'ext4',
'device_type': 'disk',
'delete_on_termination': False,
},
],
'imageRef': '1',
'min_count': 1,
'max_count': 1,
}},
)
def test_boot_no_image_bdms_v2(self):
self.run_command(
'boot --flavor 1 --block-device id=fake-id,source=volume,'
'dest=volume,bus=virtio,device=vda,size=1,format=ext4,bootindex=0,'
'type=disk,shutdown=preserve some-server'
)
self.assert_called_anytime(
'POST', '/os-volumes_boot',
{'server': {
'flavorRef': '1',
'name': 'some-server',
'block_device_mapping_v2': [
{
'uuid': 'fake-id',
'source_type': 'volume',
'destination_type': 'volume',
'disk_bus': 'virtio',
'device_name': 'vda',
'volume_size': '1',
'guest_format': 'ext4',
'boot_index': '0',
'device_type': 'disk',
'delete_on_termination': False,
}
],
'imageRef': '',
'min_count': 1,
'max_count': 1,
}},
)
cmd = 'boot --flavor 1 --boot-volume fake-id some-server'
self.run_command(cmd)
self.assert_called_anytime(
'POST', '/os-volumes_boot',
{'server': {
'flavorRef': '1',
'name': 'some-server',
'block_device_mapping_v2': [
{
'uuid': 'fake-id',
'source_type': 'volume',
'destination_type': 'volume',
'boot_index': 0,
'delete_on_termination': False,
}
],
'imageRef': '',
'min_count': 1,
'max_count': 1,
}},
)
cmd = 'boot --flavor 1 --snapshot fake-id some-server'
self.run_command(cmd)
self.assert_called_anytime(
'POST', '/os-volumes_boot',
{'server': {
'flavorRef': '1',
'name': 'some-server',
'block_device_mapping_v2': [
{
'uuid': 'fake-id',
'source_type': 'snapshot',
'destination_type': 'volume',
'boot_index': 0,
'delete_on_termination': False,
}
],
'imageRef': '',
'min_count': 1,
'max_count': 1,
}},
)
self.run_command('boot --flavor 1 --swap 1 some-server')
self.assert_called_anytime(
'POST', '/os-volumes_boot',
{'server': {
'flavorRef': '1',
'name': 'some-server',
'block_device_mapping_v2': [
{
'source_type': 'blank',
'destination_type': 'local',
'boot_index': -1,
'guest_format': 'swap',
'volume_size': '1',
'delete_on_termination': True,
}
],
'imageRef': '',
'min_count': 1,
'max_count': 1,
}},
)
self.run_command(
'boot --flavor 1 --ephemeral size=1,format=ext4 some-server'
)
self.assert_called_anytime(
'POST', '/os-volumes_boot',
{'server': {
'flavorRef': '1',
'name': 'some-server',
'block_device_mapping_v2': [
{
'source_type': 'blank',
'destination_type': 'local',
'boot_index': -1,
'guest_format': 'ext4',
'volume_size': '1',
'delete_on_termination': True,
}
],
'imageRef': '',
'min_count': 1,
'max_count': 1,
}},
)
def test_boot_metadata(self):
self.run_command('boot --image 1 --flavor 1 --meta foo=bar=pants'
' --meta spam=eggs some-server ')

View File

@ -583,7 +583,8 @@ class ServerManager(base.BootingManagerWithFind):
reservation_id=None, min_count=None,
max_count=None, security_groups=None, userdata=None,
key_name=None, availability_zone=None,
block_device_mapping=None, nics=None, scheduler_hints=None,
block_device_mapping=None, block_device_mapping_v2=None,
nics=None, scheduler_hints=None,
config_drive=None, disk_config=None, **kwargs):
# TODO(anthony): indicate in doc string if param is an extension
# and/or optional
@ -611,6 +612,8 @@ class ServerManager(base.BootingManagerWithFind):
placement.
:param block_device_mapping: (optional extension) A dict of block
device mappings for this server.
:param block_device_mapping_v2: (optional extension) A dict of block
device mappings for this server.
:param nics: (optional extension) an ordered list of nics to be
added to this server, with information about
connected networks, fixed ips, port etc.
@ -642,6 +645,9 @@ class ServerManager(base.BootingManagerWithFind):
if block_device_mapping:
resource_url = "/os-volumes_boot"
boot_kwargs['block_device_mapping'] = block_device_mapping
elif block_device_mapping_v2:
resource_url = "/os-volumes_boot"
boot_kwargs['block_device_mapping_v2'] = block_device_mapping_v2
else:
resource_url = "/servers"
if nics:

View File

@ -36,6 +36,20 @@ from novaclient.v1_1 import quotas
from novaclient.v1_1 import servers
CLIENT_BDM2_KEYS = {
'id': 'uuid',
'source': 'source_type',
'dest': 'destination_type',
'bus': 'disk_bus',
'device': 'device_name',
'size': 'volume_size',
'format': 'guest_format',
'bootindex': 'boot_index',
'type': 'device_type',
'shutdown': 'delete_on_termination',
}
def _key_value_pairing(text):
try:
(k, v) = text.split('=', 1)
@ -58,6 +72,66 @@ def _match_image(cs, wanted_properties):
return images_matched
def _parse_block_device_mapping_v2(args, image):
bdm = []
if args.boot_volume:
bdm_dict = {'uuid': args.boot_volume, 'source_type': 'volume',
'destination_type': 'volume', 'boot_index': 0,
'delete_on_termination': False}
bdm.append(bdm_dict)
if args.snapshot:
bdm_dict = {'uuid': args.snapshot, 'source_type': 'snapshot',
'destination_type': 'volume', 'boot_index': 0,
'delete_on_termination': False}
bdm.append(bdm_dict)
for device_spec in args.block_device:
spec_dict = dict(v.split('=') for v in device_spec.split(','))
bdm_dict = {}
for key, value in spec_dict.iteritems():
bdm_dict[CLIENT_BDM2_KEYS[key]] = value
# Convert the delete_on_termination to a boolean or set it to true by
# default for local block devices when not specified.
if 'delete_on_termination' in bdm_dict:
action = bdm_dict['delete_on_termination']
bdm_dict['delete_on_termination'] = (action == 'remove')
elif bdm_dict.get('destination_type') == 'local':
bdm_dict['delete_on_termination'] = True
bdm.append(bdm_dict)
for ephemeral_spec in args.ephemeral:
bdm_dict = {'source_type': 'blank', 'destination_type': 'local',
'boot_index': -1, 'delete_on_termination': True}
eph_dict = dict(v.split('=') for v in ephemeral_spec.split(','))
if 'size' in eph_dict:
bdm_dict['volume_size'] = eph_dict['size']
if 'format' in eph_dict:
bdm_dict['guest_format'] = eph_dict['format']
bdm.append(bdm_dict)
if args.swap:
bdm_dict = {'source_type': 'blank', 'destination_type': 'local',
'boot_index': -1, 'delete_on_termination': True,
'guest_format': 'swap', 'volume_size': args.swap}
bdm.append(bdm_dict)
# Append the image to the list only if we have new style BDMs
if bdm and not args.block_device_mapping and image:
bdm_dict = {'uuid': image.id, 'source_type': 'image',
'destination_type': 'local', 'boot_index': 0,
'delete_on_termination': True}
bdm.insert(0, bdm_dict)
return bdm
def _boot(cs, args, reservation_id=None, min_count=None, max_count=None):
"""Boot a new server."""
if min_count is None:
@ -83,11 +157,6 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None):
# are selecting the first of many?
image = images[0]
if not image and not args.block_device_mapping:
raise exceptions.CommandError("you need to specify an Image ID "
"or a block device mapping "
"or provide a set of properties to match"
" against an image")
if not args.flavor:
raise exceptions.CommandError("you need to specify a Flavor ID ")
@ -140,6 +209,25 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None):
device_name, mapping = bdm.split('=', 1)
block_device_mapping[device_name] = mapping
block_device_mapping_v2 = _parse_block_device_mapping_v2(args, image)
n_boot_args = len(filter(None, (image, args.boot_volume, args.snapshot)))
have_bdm = block_device_mapping_v2 or block_device_mapping
# Fail if more than one boot devices are present
# or if there is no device to boot from.
if n_boot_args > 1 or n_boot_args == 0 and not have_bdm:
raise exceptions.CommandError(
"you need to specify at least one source ID (Image, Snapshot or "
"Volume), a block device mapping or provide a set of properties "
"to match against an image")
if block_device_mapping and block_device_mapping_v2:
raise exceptions.CommandError(
"you can't mix old block devices (--block-device-mapping) "
"with the new ones (--block-device, --boot-volume, --snapshot, "
"--ephemeral, --swap)")
nics = []
for nic_str in args.nics:
err_msg = ("Invalid nic argument '%s'. Nic arguments must be of the "
@ -196,6 +284,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None):
availability_zone=availability_zone,
security_groups=security_groups,
block_device_mapping=block_device_mapping,
block_device_mapping_v2=block_device_mapping_v2,
nics=nics,
scheduler_hints=hints,
config_drive=config_drive)
@ -217,6 +306,14 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None):
action='append',
metavar='<key=value>',
help="Image metadata property (see 'nova image-show'). ")
@utils.arg('--boot-volume',
default=None,
metavar="<volume_id>",
help="Volume ID to boot from.")
@utils.arg('--snapshot',
default=None,
metavar="<snapshot_id>",
help="Sapshot ID to boot from (will create a volume).")
@utils.arg('--num-instances',
default=None,
type=int,
@ -269,6 +366,31 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None):
@utils.arg('--block_device_mapping',
action='append',
help=argparse.SUPPRESS)
@utils.arg('--block-device',
metavar="key1=value1[,key2=value2...]",
action='append',
default=[],
help="Block device mapping with the keys: "
"id=image_id, snapshot_id or volume_id, "
"source=source type (image, snapshot, volume or blank), "
"dest=destination type of the block device (volume or local), "
"bus=device's bus, "
"device=name of the device (e.g. vda, xda, ...), "
"size=size of the block device in GB, "
"format=device will be formatted (e.g. swap, ext3, ntfs, ...), "
"bootindex=integer used for ordering the boot disks, "
"type=device type (e.g. disk, cdrom, ...) and "
"shutdown=shutdown behaviour (either preserve or remove).")
@utils.arg('--swap',
metavar="<swap_size>",
default=None,
help="Create and attach a local swap block device of <swap_size> MB.")
@utils.arg('--ephemeral',
metavar="size=<size>[,format=<format>]",
action='append',
default=[],
help="Create and attach a local ephemeral block device of <size> GB "
"and format it to <format>.")
@utils.arg('--hint',
action='append',
dest='scheduler_hints',