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:
parent
d770bb3aab
commit
6a85c954c5
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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 ')
|
||||
|
@ -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:
|
||||
|
@ -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',
|
||||
|
Loading…
x
Reference in New Issue
Block a user