compute: Add missing 'server create' options

Add some volume-related options, namely '--snapshot', '--swap', and
'--ephemeral'. All are shortcuts to avoid having to use
'--block-device-mapping'.

Change-Id: I450e429ade46a7103740150c90e3ba9f2894e1a5
Signed-off-by: Stephen Finucane <sfinucan@redhat.com>
This commit is contained in:
Stephen Finucane
2020-12-10 12:40:59 +00:00
parent 074e045c69
commit 4da4b96296
3 changed files with 335 additions and 14 deletions

View File

@@ -772,6 +772,19 @@ class CreateServer(command.ShowOne):
'volume.'
),
)
disk_group.add_argument(
'--snapshot',
metavar='<snapshot>',
help=_(
'Create server using this snapshot as the boot disk (name or '
'ID)\n'
'This option automatically creates a block device mapping '
'with a boot index of 0. On many hypervisors (libvirt/kvm '
'for example) this will be device vda. Do not create a '
'duplicate mapping using --block-device-mapping for this '
'volume.'
),
)
parser.add_argument(
'--boot-from-volume',
metavar='<volume-size>',
@@ -784,7 +797,8 @@ class CreateServer(command.ShowOne):
'given size (in GB) from the specified image and use it '
'as the root disk of the server. The root volume will not '
'be deleted when the server is deleted. This option is '
'mutually exclusive with the ``--volume`` option.'
'mutually exclusive with the ``--volume`` and ``--snapshot`` '
'options.'
)
)
parser.add_argument(
@@ -810,6 +824,28 @@ class CreateServer(command.ShowOne):
'(optional)\n'
),
)
parser.add_argument(
'--swap',
metavar='<swap>',
type=int,
help=(
"Create and attach a local swap block device of <swap_size> "
"MiB."
),
)
parser.add_argument(
'--ephemeral',
metavar='<size=size[,format=format]>',
action=parseractions.MultiKeyValueAction,
dest='ephemerals',
default=[],
required_keys=['size'],
optional_keys=['format'],
help=(
"Create and attach a local ephemeral block device of <size> "
"GiB and format it to <format>."
),
)
parser.add_argument(
'--network',
metavar="<network>",
@@ -929,12 +965,14 @@ class CreateServer(command.ShowOne):
parser.add_argument(
'--availability-zone',
metavar='<zone-name>',
help=_('Select an availability zone for the server. '
'Host and node are optional parameters. '
'Availability zone in the format '
'<zone-name>:<host-name>:<node-name>, '
'<zone-name>::<node-name>, <zone-name>:<host-name> '
'or <zone-name>'),
help=_(
'Select an availability zone for the server. '
'Host and node are optional parameters. '
'Availability zone in the format '
'<zone-name>:<host-name>:<node-name>, '
'<zone-name>::<node-name>, <zone-name>:<host-name> '
'or <zone-name>'
),
)
parser.add_argument(
'--host',
@@ -1000,11 +1038,6 @@ class CreateServer(command.ShowOne):
default=1,
help=_('Maximum number of servers to launch (default=1)'),
)
parser.add_argument(
'--wait',
action='store_true',
help=_('Wait for build to complete'),
)
parser.add_argument(
'--tag',
metavar='<tag>',
@@ -1017,6 +1050,11 @@ class CreateServer(command.ShowOne):
'(supported by --os-compute-api-version 2.52 or above)'
),
)
parser.add_argument(
'--wait',
action='store_true',
help=_('Wait for build to complete'),
)
return parser
def take_action(self, parsed_args):
@@ -1092,7 +1130,6 @@ class CreateServer(command.ShowOne):
)
raise exceptions.CommandError(msg)
# Lookup parsed_args.volume
volume = None
if parsed_args.volume:
# --volume and --boot-from-volume are mutually exclusive.
@@ -1105,7 +1142,18 @@ class CreateServer(command.ShowOne):
parsed_args.volume,
).id
# Lookup parsed_args.flavor
snapshot = None
if parsed_args.snapshot:
# --snapshot and --boot-from-volume are mutually exclusive.
if parsed_args.boot_from_volume:
msg = _('--snapshot is not allowed with --boot-from-volume')
raise exceptions.CommandError(msg)
snapshot = utils.find_resource(
volume_client.volume_snapshots,
parsed_args.snapshot,
).id
flavor = utils.find_resource(
compute_client.flavors, parsed_args.flavor)
@@ -1156,6 +1204,14 @@ class CreateServer(command.ShowOne):
'source_type': 'volume',
'destination_type': 'volume'
}]
elif snapshot:
block_device_mapping_v2 = [{
'uuid': snapshot,
'boot_index': '0',
'source_type': 'snapshot',
'destination_type': 'volume',
'delete_on_termination': False
}]
elif parsed_args.boot_from_volume:
# Tell nova to create a root volume from the image provided.
block_device_mapping_v2 = [{
@@ -1168,6 +1224,30 @@ class CreateServer(command.ShowOne):
# If booting from volume we do not pass an image to compute.
image = None
if parsed_args.swap:
block_device_mapping_v2.append({
'boot_index': -1,
'source_type': 'blank',
'destination_type': 'local',
'guest_format': 'swap',
'volume_size': parsed_args.swap,
'delete_on_termination': True,
})
for mapping in parsed_args.ephemerals:
block_device_mapping_dict = {
'boot_index': -1,
'source_type': 'blank',
'destination_type': 'local',
'delete_on_termination': True,
'volume_size': mapping['size'],
}
if 'format' in mapping:
block_device_mapping_dict['guest_format'] = mapping['format']
block_device_mapping_v2.append(block_device_mapping_dict)
# Handle block device by device name order, like: vdb -> vdc -> vdd
for mapping in parsed_args.block_device_mapping:
if mapping['source_type'] == 'volume':

View File

@@ -1925,6 +1925,109 @@ class TestServerCreate(TestServer):
self.assertEqual(self.columns, columns)
self.assertEqual(self.datalist(), data)
def test_server_create_with_volume(self):
arglist = [
'--flavor', self.flavor.id,
'--volume', self.volume.name,
self.new_server.name,
]
verifylist = [
('flavor', self.flavor.id),
('volume', self.volume.name),
('server_name', self.new_server.name),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
# CreateServer.take_action() returns two tuples
columns, data = self.cmd.take_action(parsed_args)
# Set expected values
kwargs = {
'meta': None,
'files': {},
'reservation_id': None,
'min_count': 1,
'max_count': 1,
'security_groups': [],
'userdata': None,
'key_name': None,
'availability_zone': None,
'admin_pass': None,
'block_device_mapping_v2': [{
'uuid': self.volume.id,
'boot_index': '0',
'source_type': 'volume',
'destination_type': 'volume',
}],
'nics': [],
'scheduler_hints': {},
'config_drive': None,
}
# ServerManager.create(name, image, flavor, **kwargs)
self.servers_mock.create.assert_called_with(
self.new_server.name,
None,
self.flavor,
**kwargs
)
self.volumes_mock.get.assert_called_once_with(
self.volume.name)
self.assertEqual(self.columns, columns)
self.assertEqual(self.datalist(), data)
def test_server_create_with_snapshot(self):
arglist = [
'--flavor', self.flavor.id,
'--snapshot', self.snapshot.name,
self.new_server.name,
]
verifylist = [
('flavor', self.flavor.id),
('snapshot', self.snapshot.name),
('server_name', self.new_server.name),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
# CreateServer.take_action() returns two tuples
columns, data = self.cmd.take_action(parsed_args)
# Set expected values
kwargs = {
'meta': None,
'files': {},
'reservation_id': None,
'min_count': 1,
'max_count': 1,
'security_groups': [],
'userdata': None,
'key_name': None,
'availability_zone': None,
'admin_pass': None,
'block_device_mapping_v2': [{
'uuid': self.snapshot.id,
'boot_index': '0',
'source_type': 'snapshot',
'destination_type': 'volume',
'delete_on_termination': False,
}],
'nics': [],
'scheduler_hints': {},
'config_drive': None,
}
# ServerManager.create(name, image, flavor, **kwargs)
self.servers_mock.create.assert_called_with(
self.new_server.name,
None,
self.flavor,
**kwargs
)
self.snapshots_mock.get.assert_called_once_with(
self.snapshot.name)
self.assertEqual(self.columns, columns)
self.assertEqual(self.datalist(), data)
def test_server_create_with_block_device_mapping(self):
arglist = [
'--image', 'image1',
@@ -2575,6 +2678,136 @@ class TestServerCreate(TestServer):
self.assertEqual(self.columns, columns)
self.assertEqual(self.datalist(), data)
def test_server_create_with_swap(self):
arglist = [
'--image', 'image1',
'--flavor', self.flavor.id,
'--swap', '1024',
self.new_server.name,
]
verifylist = [
('image', 'image1'),
('flavor', self.flavor.id),
('swap', 1024),
('server_name', self.new_server.name),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
# CreateServer.take_action() returns two tuples
columns, data = self.cmd.take_action(parsed_args)
# Set expected values
kwargs = {
'meta': None,
'files': {},
'reservation_id': None,
'min_count': 1,
'max_count': 1,
'security_groups': [],
'userdata': None,
'key_name': None,
'availability_zone': None,
'admin_pass': None,
'block_device_mapping_v2': [{
'boot_index': -1,
'source_type': 'blank',
'destination_type': 'local',
'guest_format': 'swap',
'volume_size': 1024,
'delete_on_termination': True,
}],
'nics': [],
'scheduler_hints': {},
'config_drive': None,
}
# ServerManager.create(name, image, flavor, **kwargs)
self.servers_mock.create.assert_called_with(
self.new_server.name,
self.image,
self.flavor,
**kwargs
)
self.assertEqual(self.columns, columns)
self.assertEqual(self.datalist(), data)
def test_server_create_with_ephemeral(self):
arglist = [
'--image', 'image1',
'--flavor', self.flavor.id,
'--ephemeral', 'size=1024,format=ext4',
self.new_server.name,
]
verifylist = [
('image', 'image1'),
('flavor', self.flavor.id),
('ephemerals', [{'size': '1024', 'format': 'ext4'}]),
('server_name', self.new_server.name),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
# CreateServer.take_action() returns two tuples
columns, data = self.cmd.take_action(parsed_args)
# Set expected values
kwargs = {
'meta': None,
'files': {},
'reservation_id': None,
'min_count': 1,
'max_count': 1,
'security_groups': [],
'userdata': None,
'key_name': None,
'availability_zone': None,
'admin_pass': None,
'block_device_mapping_v2': [{
'boot_index': -1,
'source_type': 'blank',
'destination_type': 'local',
'guest_format': 'ext4',
'volume_size': '1024',
'delete_on_termination': True,
}],
'nics': [],
'scheduler_hints': {},
'config_drive': None,
}
# ServerManager.create(name, image, flavor, **kwargs)
self.servers_mock.create.assert_called_with(
self.new_server.name,
self.image,
self.flavor,
**kwargs
)
self.assertEqual(self.columns, columns)
self.assertEqual(self.datalist(), data)
def test_server_create_with_ephemeral_missing_key(self):
arglist = [
'--image', 'image1',
'--flavor', self.flavor.id,
'--ephemeral', 'format=ext3',
self.new_server.name,
]
self.assertRaises(
argparse.ArgumentTypeError,
self.check_parser,
self.cmd, arglist, [])
def test_server_create_with_ephemeral_invalid_key(self):
arglist = [
'--image', 'image1',
'--flavor', self.flavor.id,
'--ephemeral', 'size=1024,foo=bar',
self.new_server.name,
]
self.assertRaises(
argparse.ArgumentTypeError,
self.check_parser,
self.cmd, arglist, [])
def test_server_create_invalid_hint(self):
# Not a key-value pair
arglist = [

View File

@@ -0,0 +1,8 @@
---
features:
- |
Add a number of additional options to the ``server create`` command:
- ``--snapshot``
- ``--ephemeral``
- ``--swap``