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.'
|
'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(
|
parser.add_argument(
|
||||||
'--boot-from-volume',
|
'--boot-from-volume',
|
||||||
metavar='<volume-size>',
|
metavar='<volume-size>',
|
||||||
@@ -784,7 +797,8 @@ class CreateServer(command.ShowOne):
|
|||||||
'given size (in GB) from the specified image and use it '
|
'given size (in GB) from the specified image and use it '
|
||||||
'as the root disk of the server. The root volume will not '
|
'as the root disk of the server. The root volume will not '
|
||||||
'be deleted when the server is deleted. This option is '
|
'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(
|
parser.add_argument(
|
||||||
@@ -810,6 +824,28 @@ class CreateServer(command.ShowOne):
|
|||||||
'(optional)\n'
|
'(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(
|
parser.add_argument(
|
||||||
'--network',
|
'--network',
|
||||||
metavar="<network>",
|
metavar="<network>",
|
||||||
@@ -929,12 +965,14 @@ class CreateServer(command.ShowOne):
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--availability-zone',
|
'--availability-zone',
|
||||||
metavar='<zone-name>',
|
metavar='<zone-name>',
|
||||||
help=_('Select an availability zone for the server. '
|
help=_(
|
||||||
'Host and node are optional parameters. '
|
'Select an availability zone for the server. '
|
||||||
'Availability zone in the format '
|
'Host and node are optional parameters. '
|
||||||
'<zone-name>:<host-name>:<node-name>, '
|
'Availability zone in the format '
|
||||||
'<zone-name>::<node-name>, <zone-name>:<host-name> '
|
'<zone-name>:<host-name>:<node-name>, '
|
||||||
'or <zone-name>'),
|
'<zone-name>::<node-name>, <zone-name>:<host-name> '
|
||||||
|
'or <zone-name>'
|
||||||
|
),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--host',
|
'--host',
|
||||||
@@ -1000,11 +1038,6 @@ class CreateServer(command.ShowOne):
|
|||||||
default=1,
|
default=1,
|
||||||
help=_('Maximum number of servers to launch (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(
|
parser.add_argument(
|
||||||
'--tag',
|
'--tag',
|
||||||
metavar='<tag>',
|
metavar='<tag>',
|
||||||
@@ -1017,6 +1050,11 @@ class CreateServer(command.ShowOne):
|
|||||||
'(supported by --os-compute-api-version 2.52 or above)'
|
'(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
|
return parser
|
||||||
|
|
||||||
def take_action(self, parsed_args):
|
def take_action(self, parsed_args):
|
||||||
@@ -1092,7 +1130,6 @@ class CreateServer(command.ShowOne):
|
|||||||
)
|
)
|
||||||
raise exceptions.CommandError(msg)
|
raise exceptions.CommandError(msg)
|
||||||
|
|
||||||
# Lookup parsed_args.volume
|
|
||||||
volume = None
|
volume = None
|
||||||
if parsed_args.volume:
|
if parsed_args.volume:
|
||||||
# --volume and --boot-from-volume are mutually exclusive.
|
# --volume and --boot-from-volume are mutually exclusive.
|
||||||
@@ -1105,7 +1142,18 @@ class CreateServer(command.ShowOne):
|
|||||||
parsed_args.volume,
|
parsed_args.volume,
|
||||||
).id
|
).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(
|
flavor = utils.find_resource(
|
||||||
compute_client.flavors, parsed_args.flavor)
|
compute_client.flavors, parsed_args.flavor)
|
||||||
|
|
||||||
@@ -1156,6 +1204,14 @@ class CreateServer(command.ShowOne):
|
|||||||
'source_type': 'volume',
|
'source_type': 'volume',
|
||||||
'destination_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:
|
elif parsed_args.boot_from_volume:
|
||||||
# Tell nova to create a root volume from the image provided.
|
# Tell nova to create a root volume from the image provided.
|
||||||
block_device_mapping_v2 = [{
|
block_device_mapping_v2 = [{
|
||||||
@@ -1168,6 +1224,30 @@ class CreateServer(command.ShowOne):
|
|||||||
# If booting from volume we do not pass an image to compute.
|
# If booting from volume we do not pass an image to compute.
|
||||||
image = None
|
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
|
# Handle block device by device name order, like: vdb -> vdc -> vdd
|
||||||
for mapping in parsed_args.block_device_mapping:
|
for mapping in parsed_args.block_device_mapping:
|
||||||
if mapping['source_type'] == 'volume':
|
if mapping['source_type'] == 'volume':
|
||||||
|
@@ -1925,6 +1925,109 @@ class TestServerCreate(TestServer):
|
|||||||
self.assertEqual(self.columns, columns)
|
self.assertEqual(self.columns, columns)
|
||||||
self.assertEqual(self.datalist(), data)
|
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):
|
def test_server_create_with_block_device_mapping(self):
|
||||||
arglist = [
|
arglist = [
|
||||||
'--image', 'image1',
|
'--image', 'image1',
|
||||||
@@ -2575,6 +2678,136 @@ class TestServerCreate(TestServer):
|
|||||||
self.assertEqual(self.columns, columns)
|
self.assertEqual(self.columns, columns)
|
||||||
self.assertEqual(self.datalist(), data)
|
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):
|
def test_server_create_invalid_hint(self):
|
||||||
# Not a key-value pair
|
# Not a key-value pair
|
||||||
arglist = [
|
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