summaryrefslogtreecommitdiff
path: root/openstackclient
diff options
context:
space:
mode:
Diffstat (limited to 'openstackclient')
-rw-r--r--openstackclient/compute/v2/server.py108
-rw-r--r--openstackclient/tests/unit/compute/v2/test_server.py233
2 files changed, 327 insertions, 14 deletions
diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py
index 7edbb18e..1e4f1936 100644
--- a/openstackclient/compute/v2/server.py
+++ b/openstackclient/compute/v2/server.py
@@ -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(
@@ -811,6 +825,28 @@ class CreateServer(command.ShowOne):
),
)
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>",
dest='nics',
@@ -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',
@@ -1001,11 +1039,6 @@ class CreateServer(command.ShowOne):
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>',
action='append',
@@ -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':
diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py
index 58daf531..ce93f21e 100644
--- a/openstackclient/tests/unit/compute/v2/test_server.py
+++ b/openstackclient/tests/unit/compute/v2/test_server.py
@@ -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 = [