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:
@@ -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':
|
||||
|
@@ -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 = [
|
||||
|
@@ -0,0 +1,8 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Add a number of additional options to the ``server create`` command:
|
||||
|
||||
- ``--snapshot``
|
||||
- ``--ephemeral``
|
||||
- ``--swap``
|
Reference in New Issue
Block a user