summaryrefslogtreecommitdiff
path: root/openstackclient
diff options
context:
space:
mode:
Diffstat (limited to 'openstackclient')
-rw-r--r--openstackclient/compute/v2/server.py1124
-rw-r--r--openstackclient/compute/v2/server_event.py178
-rw-r--r--openstackclient/compute/v2/server_group.py38
-rw-r--r--openstackclient/network/v2/network_rbac.py22
-rw-r--r--openstackclient/network/v2/port.py7
-rw-r--r--openstackclient/network/v2/subnet.py7
-rw-r--r--openstackclient/tests/functional/compute/v2/test_server.py164
-rw-r--r--openstackclient/tests/unit/compute/v2/fakes.py73
-rw-r--r--openstackclient/tests/unit/compute/v2/test_server.py1440
-rw-r--r--openstackclient/tests/unit/compute/v2/test_server_event.py235
-rw-r--r--openstackclient/tests/unit/compute/v2/test_server_group.py47
-rw-r--r--openstackclient/tests/unit/network/v2/test_network_rbac.py6
-rw-r--r--openstackclient/tests/unit/network/v2/test_port.py20
-rw-r--r--openstackclient/tests/unit/network/v2/test_subnet.py4
14 files changed, 2734 insertions, 631 deletions
diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py
index 02fc5816..b81d2a18 100644
--- a/openstackclient/compute/v2/server.py
+++ b/openstackclient/compute/v2/server.py
@@ -18,6 +18,7 @@
import argparse
import getpass
import io
+import json
import logging
import os
@@ -31,6 +32,7 @@ from osc_lib.cli import parseractions
from osc_lib.command import command
from osc_lib import exceptions
from osc_lib import utils
+from oslo_utils import strutils
from openstackclient.i18n import _
from openstackclient.identity import common as identity_common
@@ -97,16 +99,6 @@ def _get_ip_address(addresses, address_type, ip_address_family):
)
-def _prefix_checked_value(prefix):
- def func(value):
- if ',' in value or '=' in value:
- msg = _("Invalid argument %s, "
- "characters ',' and '=' are not allowed") % value
- raise argparse.ArgumentTypeError(msg)
- return prefix + value
- return func
-
-
def _prep_server_detail(compute_client, image_client, server, refresh=True):
"""Prepare the detailed server dict for printing
@@ -193,6 +185,24 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True):
return info
+def boolenv(*vars, default=False):
+ """Search for the first defined of possibly many bool-like env vars.
+
+ Returns the first environment variable defined in vars, or returns the
+ default.
+
+ :param vars: Arbitrary strings to search for. Case sensitive.
+ :param default: The default to return if no value found.
+ :returns: A boolean corresponding to the value found, else the default if
+ no value found.
+ """
+ for v in vars:
+ value = os.environ.get(v, None)
+ if value:
+ return strutils.bool_from_string(value)
+ return default
+
+
class AddFixedIP(command.Command):
_description = _("Add fixed IP address to server")
@@ -592,6 +602,189 @@ class AddServerVolume(command.Command):
)
+# TODO(stephenfin): Replace with 'MultiKeyValueAction' when we no longer
+# support '--nic=auto' and '--nic=none'
+class NICAction(argparse.Action):
+
+ def __init__(
+ self,
+ option_strings,
+ dest,
+ nargs=None,
+ const=None,
+ default=None,
+ type=None,
+ choices=None,
+ required=False,
+ help=None,
+ metavar=None,
+ key=None,
+ ):
+ self.key = key
+ super().__init__(
+ option_strings=option_strings, dest=dest, nargs=nargs, const=const,
+ default=default, type=type, choices=choices, required=required,
+ help=help, metavar=metavar,
+ )
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ # Make sure we have an empty dict rather than None
+ if getattr(namespace, self.dest, None) is None:
+ setattr(namespace, self.dest, [])
+
+ # Handle the special auto/none cases
+ if values in ('auto', 'none'):
+ getattr(namespace, self.dest).append(values)
+ return
+
+ if self.key:
+ if ',' in values or '=' in values:
+ msg = _(
+ "Invalid argument %s; characters ',' and '=' are not "
+ "allowed"
+ )
+ raise argparse.ArgumentTypeError(msg % values)
+
+ values = '='.join([self.key, values])
+
+ # We don't include 'tag' here by default since that requires a
+ # particular microversion
+ info = {
+ 'net-id': '',
+ 'port-id': '',
+ 'v4-fixed-ip': '',
+ 'v6-fixed-ip': '',
+ }
+
+ for kv_str in values.split(','):
+ k, sep, v = kv_str.partition("=")
+
+ if k not in list(info) + ['tag'] or not v:
+ msg = _(
+ "Invalid argument %s; argument must be of form "
+ "'net-id=net-uuid,port-id=port-uuid,v4-fixed-ip=ip-addr,"
+ "v6-fixed-ip=ip-addr,tag=tag'"
+ )
+ raise argparse.ArgumentTypeError(msg % values)
+
+ info[k] = v
+
+ if info['net-id'] and info['port-id']:
+ msg = _(
+ 'Invalid argument %s; either network or port should be '
+ 'specified but not both'
+ )
+ raise argparse.ArgumentTypeError(msg % values)
+
+ getattr(namespace, self.dest).append(info)
+
+
+class BDMLegacyAction(argparse.Action):
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ # Make sure we have an empty list rather than None
+ if getattr(namespace, self.dest, None) is None:
+ setattr(namespace, self.dest, [])
+
+ dev_name, sep, dev_map = values.partition('=')
+ dev_map = dev_map.split(':') if dev_map else dev_map
+ if not dev_name or not dev_map or len(dev_map) > 4:
+ msg = _(
+ "Invalid argument %s; argument must be of form "
+ "'dev-name=id[:type[:size[:delete-on-terminate]]]'"
+ )
+ raise argparse.ArgumentTypeError(msg % values)
+
+ mapping = {
+ 'device_name': dev_name,
+ # store target; this may be a name and will need verification later
+ 'uuid': dev_map[0],
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ }
+
+ # decide source and destination type
+ if len(dev_map) > 1 and dev_map[1]:
+ if dev_map[1] not in ('volume', 'snapshot', 'image'):
+ msg = _(
+ "Invalid argument %s; 'type' must be one of: volume, "
+ "snapshot, image"
+ )
+ raise argparse.ArgumentTypeError(msg % values)
+
+ mapping['source_type'] = dev_map[1]
+
+ # 3. append size and delete_on_termination, if present
+ if len(dev_map) > 2 and dev_map[2]:
+ mapping['volume_size'] = dev_map[2]
+
+ if len(dev_map) > 3 and dev_map[3]:
+ mapping['delete_on_termination'] = dev_map[3]
+
+ getattr(namespace, self.dest).append(mapping)
+
+
+class BDMAction(parseractions.MultiKeyValueAction):
+
+ def __init__(self, option_strings, dest, **kwargs):
+ required_keys = []
+ optional_keys = [
+ 'uuid', 'source_type', 'destination_type',
+ 'disk_bus', 'device_type', 'device_name', 'volume_size',
+ 'guest_format', 'boot_index', 'delete_on_termination', 'tag',
+ 'volume_type',
+ ]
+ super().__init__(
+ option_strings, dest, required_keys=required_keys,
+ optional_keys=optional_keys, **kwargs,
+ )
+
+ # TODO(stephenfin): Remove once I549d0897ef3704b7f47000f867d6731ad15d3f2b
+ # or similar lands in a release
+ def validate_keys(self, keys):
+ """Validate the provided keys.
+
+ :param keys: A list of keys to validate.
+ """
+ valid_keys = self.required_keys | self.optional_keys
+ invalid_keys = [k for k in keys if k not in valid_keys]
+ if invalid_keys:
+ msg = _(
+ "Invalid keys %(invalid_keys)s specified.\n"
+ "Valid keys are: %(valid_keys)s"
+ )
+ raise argparse.ArgumentTypeError(msg % {
+ 'invalid_keys': ', '.join(invalid_keys),
+ 'valid_keys': ', '.join(valid_keys),
+ })
+
+ missing_keys = [k for k in self.required_keys if k not in keys]
+ if missing_keys:
+ msg = _(
+ "Missing required keys %(missing_keys)s.\n"
+ "Required keys are: %(required_keys)s"
+ )
+ raise argparse.ArgumentTypeError(msg % {
+ 'missing_keys': ', '.join(missing_keys),
+ 'required_keys': ', '.join(self.required_keys),
+ })
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ if getattr(namespace, self.dest, None) is None:
+ setattr(namespace, self.dest, [])
+
+ if os.path.exists(values):
+ with open(values) as fh:
+ data = json.load(fh)
+
+ # Validate the keys - other validation is left to later
+ self.validate_keys(list(data))
+
+ getattr(namespace, self.dest, []).append(data)
+ else:
+ super().__call__(parser, namespace, values, option_string)
+
+
class CreateServer(command.ShowOne):
_description = _("Create a new server")
@@ -602,6 +795,12 @@ class CreateServer(command.ShowOne):
metavar='<server-name>',
help=_('New server name'),
)
+ parser.add_argument(
+ '--flavor',
+ metavar='<flavor>',
+ required=True,
+ help=_('Create server with this flavor (name or ID)'),
+ )
disk_group = parser.add_mutually_exclusive_group(
required=True,
)
@@ -610,12 +809,17 @@ class CreateServer(command.ShowOne):
metavar='<image>',
help=_('Create server boot disk from this image (name or ID)'),
)
+ # TODO(stephenfin): Is this actually useful? Looks like a straight port
+ # from 'nova boot --image-with'. Perhaps we should deprecate this.
disk_group.add_argument(
'--image-property',
metavar='<key=value>',
action=parseractions.KeyValueAction,
dest='image_properties',
- help=_("Image property to be matched"),
+ help=_(
+ "Create server using the image that matches the specified "
+ "property. Property must match exactly one property."
+ ),
)
disk_group.add_argument(
'--volume',
@@ -630,18 +834,187 @@ 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>',
+ type=int,
+ help=_(
+ 'When used in conjunction with the ``--image`` or '
+ '``--image-property`` option, this option automatically '
+ 'creates a block device mapping with a boot index of 0 '
+ 'and tells the compute service to create a volume of the '
+ '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`` and ``--snapshot`` '
+ 'options.'
+ )
+ )
+ # TODO(stephenfin): Remove this in the v7.0
+ parser.add_argument(
+ '--block-device-mapping',
+ metavar='<dev-name=mapping>',
+ action=BDMLegacyAction,
+ default=[],
+ # NOTE(RuiChen): Add '\n' to the end of line to improve formatting;
+ # see cliff's _SmartHelpFormatter for more details.
+ help=_(
+ '**Deprecated** Create a block device on the server.\n'
+ 'Block device mapping in the format\n'
+ '<dev-name>=<id>:<type>:<size(GB)>:<delete-on-terminate>\n'
+ '<dev-name>: block device name, like: vdb, xvdc '
+ '(required)\n'
+ '<id>: Name or ID of the volume, volume snapshot or image '
+ '(required)\n'
+ '<type>: volume, snapshot or image; default: volume '
+ '(optional)\n'
+ '<size(GB)>: volume size if create from image or snapshot '
+ '(optional)\n'
+ '<delete-on-terminate>: true or false; default: false '
+ '(optional)\n'
+ 'Replaced by --block-device'
+ ),
+ )
+ parser.add_argument(
+ '--block-device',
+ metavar='',
+ action=BDMAction,
+ dest='block_devices',
+ default=[],
+ help=_(
+ 'Create a block device on the server.\n'
+ 'Either a path to a JSON file or a CSV-serialized string '
+ 'describing the block device mapping.\n'
+ 'The following keys are accepted for both:\n'
+ 'uuid=<uuid>: UUID of the volume, snapshot or ID '
+ '(required if using source image, snapshot or volume),\n'
+ 'source_type=<source_type>: source type '
+ '(one of: image, snapshot, volume, blank),\n'
+ 'destination_typ=<destination_type>: destination type '
+ '(one of: volume, local) (optional),\n'
+ 'disk_bus=<disk_bus>: device bus '
+ '(one of: uml, lxc, virtio, ...) (optional),\n'
+ 'device_type=<device_type>: device type '
+ '(one of: disk, cdrom, etc. (optional),\n'
+ 'device_name=<device_name>: name of the device (optional),\n'
+ 'volume_size=<volume_size>: size of the block device in MiB '
+ '(for swap) or GiB (for everything else) (optional),\n'
+ 'guest_format=<guest_format>: format of device (optional),\n'
+ 'boot_index=<boot_index>: index of disk used to order boot '
+ 'disk '
+ '(required for volume-backed instances),\n'
+ 'delete_on_termination=<true|false>: whether to delete the '
+ 'volume upon deletion of server (optional),\n'
+ 'tag=<tag>: device metadata tag (optional),\n'
+ 'volume_type=<volume_type>: type of volume to create (name or '
+ 'ID) when source if blank, image or snapshot and dest is '
+ 'volume (optional)'
+ ),
+ )
+ 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',
+ default=[],
+ action=NICAction,
+ key='net-id',
+ # NOTE(RuiChen): Add '\n' to the end of line to improve formatting;
+ # see cliff's _SmartHelpFormatter for more details.
+ help=_(
+ "Create a NIC on the server and connect it to network. "
+ "Specify option multiple times to create multiple NICs. "
+ "This is a wrapper for the '--nic net-id=<network>' "
+ "parameter that provides simple syntax for the standard "
+ "use case of connecting a new server to a given network. "
+ "For more advanced use cases, refer to the '--nic' "
+ "parameter."
+ ),
+ )
+ parser.add_argument(
+ '--port',
+ metavar="<port>",
+ dest='nics',
+ default=[],
+ action=NICAction,
+ key='port-id',
+ help=_(
+ "Create a NIC on the server and connect it to port. "
+ "Specify option multiple times to create multiple NICs. "
+ "This is a wrapper for the '--nic port-id=<port>' "
+ "parameter that provides simple syntax for the standard "
+ "use case of connecting a new server to a given port. For "
+ "more advanced use cases, refer to the '--nic' parameter."
+ ),
+ )
+ parser.add_argument(
+ '--nic',
+ metavar="<net-id=net-uuid,port-id=port-uuid,v4-fixed-ip=ip-addr,"
+ "v6-fixed-ip=ip-addr,tag=tag,auto,none>",
+ action=NICAction,
+ dest='nics',
+ default=[],
+ help=_(
+ "Create a NIC on the server.\n"
+ "NIC in the format:\n"
+ "net-id=<net-uuid>: attach NIC to network with this UUID,\n"
+ "port-id=<port-uuid>: attach NIC to port with this UUID,\n"
+ "v4-fixed-ip=<ip-addr>: IPv4 fixed address for NIC (optional),"
+ "\n"
+ "v6-fixed-ip=<ip-addr>: IPv6 fixed address for NIC (optional),"
+ "\n"
+ "tag: interface metadata tag (optional) "
+ "(supported by --os-compute-api-version 2.43 or above),\n"
+ "none: (v2.37+) no network is attached,\n"
+ "auto: (v2.37+) the compute service will automatically "
+ "allocate a network.\n"
+ "\n"
+ "Specify option multiple times to create multiple NICs.\n"
+ "Specifying a --nic of auto or none cannot be used with any "
+ "other --nic value.\n"
+ "Either net-id or port-id must be provided, but not both."
+ ),
+ )
parser.add_argument(
'--password',
metavar='<password>',
help=_("Set the password to this server"),
)
parser.add_argument(
- '--flavor',
- metavar='<flavor>',
- required=True,
- help=_('Create server with this flavor (name or ID)'),
- )
- parser.add_argument(
'--security-group',
metavar='<security-group>',
action='append',
@@ -672,8 +1045,9 @@ class CreateServer(command.ShowOne):
action='append',
default=[],
help=_(
- 'File to inject into image before boot '
+ 'File(s) to inject into image before boot '
'(repeat option to set multiple files)'
+ '(supported by --os-compute-api-version 2.57 or below)'
),
)
parser.add_argument(
@@ -692,12 +1066,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',
@@ -718,100 +1094,11 @@ class CreateServer(command.ShowOne):
),
)
parser.add_argument(
- '--boot-from-volume',
- metavar='<volume-size>',
- type=int,
- help=_(
- 'When used in conjunction with the ``--image`` or '
- '``--image-property`` option, this option automatically '
- 'creates a block device mapping with a boot index of 0 '
- 'and tells the compute service to create a volume of the '
- '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.'
- )
- )
- parser.add_argument(
- '--block-device-mapping',
- metavar='<dev-name=mapping>',
- action=parseractions.KeyValueAction,
- default={},
- # NOTE(RuiChen): Add '\n' at the end of line to put each item in
- # the separated line, avoid the help message looks
- # messy, see _SmartHelpFormatter in cliff.
- help=_(
- 'Create a block device on the server.\n'
- 'Block device mapping in the format\n'
- '<dev-name>=<id>:<type>:<size(GB)>:<delete-on-terminate>\n'
- '<dev-name>: block device name, like: vdb, xvdc '
- '(required)\n'
- '<id>: Name or ID of the volume, volume snapshot or image '
- '(required)\n'
- '<type>: volume, snapshot or image; default: volume '
- '(optional)\n'
- '<size(GB)>: volume size if create from image or snapshot '
- '(optional)\n'
- '<delete-on-terminate>: true or false; default: false '
- '(optional)\n'
- ),
- )
- parser.add_argument(
- '--nic',
- metavar="<net-id=net-uuid,v4-fixed-ip=ip-addr,v6-fixed-ip=ip-addr,"
- "port-id=port-uuid,auto,none>",
- action='append',
- help=_(
- "Create a NIC on the server. "
- "Specify option multiple times to create multiple NICs. "
- "Either net-id or port-id must be provided, but not both. "
- "net-id: attach NIC to network with this UUID, "
- "port-id: attach NIC to port with this UUID, "
- "v4-fixed-ip: IPv4 fixed address for NIC (optional), "
- "v6-fixed-ip: IPv6 fixed address for NIC (optional), "
- "none: (v2.37+) no network is attached, "
- "auto: (v2.37+) the compute service will automatically "
- "allocate a network. Specifying a --nic of auto or none "
- "cannot be used with any other --nic value."
- ),
- )
- parser.add_argument(
- '--network',
- metavar="<network>",
- action='append',
- dest='nic',
- type=_prefix_checked_value('net-id='),
- help=_(
- "Create a NIC on the server and connect it to network. "
- "Specify option multiple times to create multiple NICs. "
- "This is a wrapper for the '--nic net-id=<network>' "
- "parameter that provides simple syntax for the standard "
- "use case of connecting a new server to a given network. "
- "For more advanced use cases, refer to the '--nic' "
- "parameter."
- ),
- )
- parser.add_argument(
- '--port',
- metavar="<port>",
- action='append',
- dest='nic',
- type=_prefix_checked_value('port-id='),
- help=_(
- "Create a NIC on the server and connect it to port. "
- "Specify option multiple times to create multiple NICs. "
- "This is a wrapper for the '--nic port-id=<port>' "
- "parameter that provides simple syntax for the standard "
- "use case of connecting a new server to a given port. For "
- "more advanced use cases, refer to the '--nic' parameter."
- ),
- )
- parser.add_argument(
'--hint',
metavar='<key=value>',
action=parseractions.KeyValueAppendAction,
default={},
- help=_('Hints for the scheduler (optional extension)'),
+ help=_('Hints for the scheduler'),
)
config_drive_group = parser.add_mutually_exclusive_group()
config_drive_group.add_argument(
@@ -853,11 +1140,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',
@@ -869,6 +1151,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):
@@ -944,7 +1231,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.
@@ -957,10 +1243,30 @@ 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)
+ if parsed_args.file:
+ if compute_client.api_version >= api_versions.APIVersion('2.57'):
+ msg = _(
+ 'Personality files are deprecated and are not supported '
+ 'for --os-compute-api-version greater than 2.56; use '
+ 'user data instead'
+ )
+ raise exceptions.CommandError(msg)
+
files = {}
for f in parsed_args.file:
dst, src = f.split('=', 1)
@@ -1008,6 +1314,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 = [{
@@ -1020,142 +1334,195 @@ class CreateServer(command.ShowOne):
# If booting from volume we do not pass an image to compute.
image = None
- boot_args = [parsed_args.server_name, image, flavor]
+ 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 dev_name in sorted(parsed_args.block_device_mapping):
- dev_map = parsed_args.block_device_mapping[dev_name]
- dev_map = dev_map.split(':')
- if dev_map[0]:
- mapping = {'device_name': dev_name}
-
- # 1. decide source and destination type
- if (len(dev_map) > 1 and
- dev_map[1] in ('volume', 'snapshot', 'image')):
- mapping['source_type'] = dev_map[1]
- else:
- mapping['source_type'] = 'volume'
-
- mapping['destination_type'] = 'volume'
-
- # 2. check target exist, update target uuid according by
- # source type
- if mapping['source_type'] == 'volume':
- volume_id = utils.find_resource(
- volume_client.volumes, dev_map[0]).id
- mapping['uuid'] = volume_id
- elif mapping['source_type'] == 'snapshot':
- snapshot_id = utils.find_resource(
- volume_client.volume_snapshots, dev_map[0]).id
- mapping['uuid'] = snapshot_id
- elif mapping['source_type'] == 'image':
- # NOTE(mriedem): In case --image is specified with the same
- # image, that becomes the root disk for the server. If the
- # block device is specified with a root device name, e.g.
- # vda, then the compute API will likely fail complaining
- # that there is a conflict. So if using the same image ID,
- # which doesn't really make sense but it's allowed, the
- # device name would need to be a non-root device, e.g. vdb.
- # Otherwise if the block device image is different from the
- # one specified by --image, then the compute service will
- # create a volume from the image and attach it to the
- # server as a non-root volume.
- image_id = image_client.find_image(dev_map[0],
- ignore_missing=False).id
- mapping['uuid'] = image_id
-
- # 3. append size and delete_on_termination if exist
- if len(dev_map) > 2 and dev_map[2]:
- mapping['volume_size'] = dev_map[2]
-
- if len(dev_map) > 3 and dev_map[3]:
- mapping['delete_on_termination'] = dev_map[3]
- else:
+ for mapping in parsed_args.block_device_mapping:
+ # The 'uuid' field isn't necessarily a UUID yet; let's validate it
+ # just in case
+ if mapping['source_type'] == 'volume':
+ volume_id = utils.find_resource(
+ volume_client.volumes, mapping['uuid'],
+ ).id
+ mapping['uuid'] = volume_id
+ elif mapping['source_type'] == 'snapshot':
+ snapshot_id = utils.find_resource(
+ volume_client.volume_snapshots, mapping['uuid'],
+ ).id
+ mapping['uuid'] = snapshot_id
+ elif mapping['source_type'] == 'image':
+ # NOTE(mriedem): In case --image is specified with the same
+ # image, that becomes the root disk for the server. If the
+ # block device is specified with a root device name, e.g.
+ # vda, then the compute API will likely fail complaining
+ # that there is a conflict. So if using the same image ID,
+ # which doesn't really make sense but it's allowed, the
+ # device name would need to be a non-root device, e.g. vdb.
+ # Otherwise if the block device image is different from the
+ # one specified by --image, then the compute service will
+ # create a volume from the image and attach it to the
+ # server as a non-root volume.
+ image_id = image_client.find_image(
+ mapping['uuid'], ignore_missing=False,
+ ).id
+ mapping['uuid'] = image_id
+
+ block_device_mapping_v2.append(mapping)
+
+ for mapping in parsed_args.block_devices:
+ if 'boot_index' in mapping:
+ try:
+ mapping['boot_index'] = int(mapping['boot_index'])
+ except ValueError:
+ msg = _(
+ 'The boot_index key of --block-device should be an '
+ 'integer'
+ )
+ raise exceptions.CommandError(msg)
+
+ if 'tag' in mapping and (
+ compute_client.api_version < api_versions.APIVersion('2.42')
+ ):
msg = _(
- 'Volume, volume snapshot or image (name or ID) must '
- 'be specified if --block-device-mapping is specified'
+ '--os-compute-api-version 2.42 or greater is '
+ 'required to support the tag key of --block-device'
)
raise exceptions.CommandError(msg)
- block_device_mapping_v2.append(mapping)
- nics = []
- auto_or_none = False
- if parsed_args.nic is None:
- parsed_args.nic = []
- for nic_str in parsed_args.nic:
- # Handle the special auto/none cases
- if nic_str in ('auto', 'none'):
- auto_or_none = True
- nics.append(nic_str)
+ if 'volume_type' in mapping and (
+ compute_client.api_version < api_versions.APIVersion('2.67')
+ ):
+ msg = _(
+ '--os-compute-api-version 2.67 or greater is '
+ 'required to support the volume_type key of --block-device'
+ )
+ raise exceptions.CommandError(msg)
+
+ if 'source_type' in mapping:
+ if mapping['source_type'] not in (
+ 'volume', 'image', 'snapshot', 'blank',
+ ):
+ msg = _(
+ 'The source_type key of --block-device should be one '
+ 'of: volume, image, snapshot, blank'
+ )
+ raise exceptions.CommandError(msg)
+ else:
+ mapping['source_type'] = 'blank'
+
+ if 'destination_type' in mapping:
+ if mapping['destination_type'] not in ('local', 'volume'):
+ msg = _(
+ 'The destination_type key of --block-device should be '
+ 'one of: local, volume'
+ )
+ raise exceptions.CommandError(msg)
else:
- nic_info = {
- 'net-id': '',
- 'v4-fixed-ip': '',
- 'v6-fixed-ip': '',
- 'port-id': '',
- }
- for kv_str in nic_str.split(","):
- k, sep, v = kv_str.partition("=")
- if k in nic_info and v:
- nic_info[k] = v
- else:
- msg = _(
- "Invalid nic argument '%s'. Nic arguments "
- "must be of the form --nic <net-id=net-uuid"
- ",v4-fixed-ip=ip-addr,v6-fixed-ip=ip-addr,"
- "port-id=port-uuid>."
- )
- raise exceptions.CommandError(msg % k)
+ if mapping['source_type'] in ('blank',):
+ mapping['destination_type'] = 'local'
+ else: # volume, image, snapshot
+ mapping['destination_type'] = 'volume'
- if bool(nic_info["net-id"]) == bool(nic_info["port-id"]):
+ if 'delete_on_termination' in mapping:
+ try:
+ value = strutils.bool_from_string(
+ mapping['delete_on_termination'], strict=True)
+ except ValueError:
msg = _(
- 'Either network or port should be specified '
- 'but not both'
+ 'The delete_on_termination key of --block-device '
+ 'should be a boolean-like value'
)
raise exceptions.CommandError(msg)
+ mapping['delete_on_termination'] = value
+ else:
+ if mapping['destination_type'] == 'local':
+ mapping['delete_on_termination'] = True
+
+ block_device_mapping_v2.append(mapping)
+
+ nics = parsed_args.nics
+
+ if 'auto' in nics or 'none' in nics:
+ if len(nics) > 1:
+ msg = _(
+ 'Specifying a --nic of auto or none cannot '
+ 'be used with any other --nic, --network '
+ 'or --port value.'
+ )
+ raise exceptions.CommandError(msg)
+
+ nics = nics[0]
+ else:
+ for nic in nics:
+ if 'tag' in nic:
+ if (
+ compute_client.api_version <
+ api_versions.APIVersion('2.43')
+ ):
+ msg = _(
+ '--os-compute-api-version 2.43 or greater is '
+ 'required to support the --nic tag field'
+ )
+ raise exceptions.CommandError(msg)
+
if self.app.client_manager.is_network_endpoint_enabled():
network_client = self.app.client_manager.network
- if nic_info["net-id"]:
+
+ if nic['net-id']:
net = network_client.find_network(
- nic_info["net-id"], ignore_missing=False)
- nic_info["net-id"] = net.id
- if nic_info["port-id"]:
+ nic['net-id'], ignore_missing=False,
+ )
+ nic['net-id'] = net.id
+
+ if nic['port-id']:
port = network_client.find_port(
- nic_info["port-id"], ignore_missing=False)
- nic_info["port-id"] = port.id
+ nic['port-id'], ignore_missing=False,
+ )
+ nic['port-id'] = port.id
else:
- if nic_info["net-id"]:
- nic_info["net-id"] = compute_client.api.network_find(
- nic_info["net-id"]
+ if nic['net-id']:
+ nic['net-id'] = compute_client.api.network_find(
+ nic['net-id'],
)['id']
- if nic_info["port-id"]:
+
+ if nic['port-id']:
msg = _(
"Can't create server with port specified "
"since network endpoint not enabled"
)
raise exceptions.CommandError(msg)
- nics.append(nic_info)
-
- if nics:
- if auto_or_none:
- if len(nics) > 1:
- msg = _(
- 'Specifying a --nic of auto or none cannot '
- 'be used with any other --nic, --network '
- 'or --port value.'
- )
- raise exceptions.CommandError(msg)
- nics = nics[0]
- else:
+ if not nics:
# Compute API version >= 2.37 requires a value, so default to
# 'auto' to maintain legacy behavior if a nic wasn't specified.
if compute_client.api_version >= api_versions.APIVersion('2.37'):
nics = 'auto'
else:
- # Default to empty list if nothing was specified, let nova
- # side to decide the default behavior.
+ # Default to empty list if nothing was specified and let nova
+ # decide the default behavior.
nics = []
# Check security group exist and convert ID to name
@@ -1196,6 +1563,8 @@ class CreateServer(command.ShowOne):
else:
config_drive = parsed_args.config_drive
+ boot_args = [parsed_args.server_name, image, flavor]
+
boot_kwargs = dict(
meta=parsed_args.properties,
files=files,
@@ -1323,6 +1692,15 @@ class DeleteServer(command.Command):
help=_('Force delete server(s)'),
)
parser.add_argument(
+ '--all-projects',
+ action='store_true',
+ default=boolenv('ALL_PROJECTS'),
+ help=_(
+ 'Delete server(s) in another project by name (admin only)'
+ '(can be specified using the ALL_PROJECTS envvar)'
+ ),
+ )
+ parser.add_argument(
'--wait',
action='store_true',
help=_('Wait for delete to complete'),
@@ -1339,7 +1717,8 @@ class DeleteServer(command.Command):
compute_client = self.app.client_manager.compute
for server in parsed_args.server:
server_obj = utils.find_resource(
- compute_client.servers, server)
+ compute_client.servers, server,
+ all_tenants=parsed_args.all_projects)
if parsed_args.force:
compute_client.servers.force_delete(server_obj.id)
@@ -1347,11 +1726,13 @@ class DeleteServer(command.Command):
compute_client.servers.delete(server_obj.id)
if parsed_args.wait:
- if not utils.wait_for_delete(compute_client.servers,
- server_obj.id,
- callback=_show_progress):
- LOG.error(_('Error deleting server: %s'),
- server_obj.id)
+ if not utils.wait_for_delete(
+ compute_client.servers,
+ server_obj.id,
+ callback=_show_progress,
+ ):
+ msg = _('Error deleting server: %s')
+ LOG.error(msg, server_obj.id)
self.app.stdout.write(_('Error deleting server\n'))
raise SystemExit
@@ -1446,8 +1827,11 @@ class ListServer(command.Lister):
parser.add_argument(
'--all-projects',
action='store_true',
- default=bool(int(os.environ.get("ALL_PROJECTS", 0))),
- help=_('Include all projects (admin only)'),
+ default=boolenv('ALL_PROJECTS'),
+ help=_(
+ 'Include all projects (admin only) '
+ '(can be specified using the ALL_PROJECTS envvar)'
+ ),
)
parser.add_argument(
'--project',
@@ -2140,64 +2524,62 @@ revert to release the new server and restart the old one.""")
'--live-migration',
dest='live_migration',
action='store_true',
- help=_('Live migrate the server. Use the ``--host`` option to '
- 'specify a target host for the migration which will be '
- 'validated by the scheduler.'),
- )
- # The --live and --host options are mutually exclusive ways of asking
- # for a target host during a live migration.
- host_group = parser.add_mutually_exclusive_group()
- # TODO(mriedem): Remove --live in the next major version bump after
- # the Train release.
- host_group.add_argument(
- '--live',
- metavar='<hostname>',
- help=_('**Deprecated** This option is problematic in that it '
- 'requires a host and prior to compute API version 2.30, '
- 'specifying a host during live migration will bypass '
- 'validation by the scheduler which could result in '
- 'failures to actually migrate the server to the specified '
- 'host or over-subscribe the host. Use the '
- '``--live-migration`` option instead. If both this option '
- 'and ``--live-migration`` are used, ``--live-migration`` '
- 'takes priority.'),
- )
- host_group.add_argument(
+ help=_(
+ 'Live migrate the server; use the ``--host`` option to '
+ 'specify a target host for the migration which will be '
+ 'validated by the scheduler'
+ ),
+ )
+ parser.add_argument(
'--host',
metavar='<hostname>',
- help=_('Migrate the server to the specified host. Requires '
- '``--os-compute-api-version`` 2.30 or greater when used '
- 'with the ``--live-migration`` option, otherwise requires '
- '``--os-compute-api-version`` 2.56 or greater.'),
+ help=_(
+ 'Migrate the server to the specified host. '
+ '(supported with --os-compute-api-version 2.30 or above '
+ 'when used with the --live-migration option) '
+ '(supported with --os-compute-api-version 2.56 or above '
+ 'when used without the --live-migration option)'
+ ),
)
migration_group = parser.add_mutually_exclusive_group()
migration_group.add_argument(
'--shared-migration',
dest='block_migration',
action='store_false',
- default=False,
- help=_('Perform a shared live migration (default)'),
+ default=None,
+ help=_(
+ 'Perform a shared live migration '
+ '(default before --os-compute-api-version 2.25, auto after)'
+ ),
)
migration_group.add_argument(
'--block-migration',
dest='block_migration',
action='store_true',
- help=_('Perform a block live migration'),
+ help=_(
+ 'Perform a block live migration '
+ '(auto-configured from --os-compute-api-version 2.25)'
+ ),
)
disk_group = parser.add_mutually_exclusive_group()
disk_group.add_argument(
'--disk-overcommit',
action='store_true',
default=False,
- help=_('Allow disk over-commit on the destination host'),
+ help=_(
+ 'Allow disk over-commit on the destination host'
+ '(supported with --os-compute-api-version 2.24 or below)'
+ ),
)
disk_group.add_argument(
'--no-disk-overcommit',
dest='disk_overcommit',
action='store_false',
default=False,
- help=_('Do not over-commit disk on the'
- ' destination host (default)'),
+ help=_(
+ 'Do not over-commit disk on the destination host (default)'
+ '(supported with --os-compute-api-version 2.24 or below)'
+ ),
)
parser.add_argument(
'--wait',
@@ -2206,15 +2588,6 @@ revert to release the new server and restart the old one.""")
)
return parser
- def _log_warning_for_live(self, parsed_args):
- if parsed_args.live:
- # NOTE(mriedem): The --live option requires a host and if
- # --os-compute-api-version is less than 2.30 it will forcefully
- # bypass the scheduler which is dangerous.
- self.log.warning(_(
- 'The --live option has been deprecated. Please use the '
- '--live-migration option instead.'))
-
def take_action(self, parsed_args):
def _show_progress(progress):
@@ -2228,36 +2601,54 @@ revert to release the new server and restart the old one.""")
compute_client.servers,
parsed_args.server,
)
- # Check for live migration.
- if parsed_args.live or parsed_args.live_migration:
- # Always log a warning if --live is used.
- self._log_warning_for_live(parsed_args)
- kwargs = {
- 'block_migration': parsed_args.block_migration
- }
- # Prefer --live-migration over --live if both are specified.
- if parsed_args.live_migration:
- # Technically we could pass a non-None host with
- # --os-compute-api-version < 2.30 but that is the same thing
- # as the --live option bypassing the scheduler which we don't
- # want to support, so if the user is using --live-migration
- # and --host, we want to enforce that they are using version
- # 2.30 or greater.
- if (parsed_args.host and
- compute_client.api_version <
- api_versions.APIVersion('2.30')):
- raise exceptions.CommandError(
- '--os-compute-api-version 2.30 or greater is required '
- 'when using --host')
- # The host parameter is required in the API even if None.
- kwargs['host'] = parsed_args.host
- else:
- kwargs['host'] = parsed_args.live
+
+ if parsed_args.live_migration:
+ kwargs = {}
+
+ block_migration = parsed_args.block_migration
+ if block_migration is None:
+ if (
+ compute_client.api_version <
+ api_versions.APIVersion('2.25')
+ ):
+ block_migration = False
+ else:
+ block_migration = 'auto'
+
+ kwargs['block_migration'] = block_migration
+
+ # Technically we could pass a non-None host with
+ # --os-compute-api-version < 2.30 but that is the same thing
+ # as the --live option bypassing the scheduler which we don't
+ # want to support, so if the user is using --live-migration
+ # and --host, we want to enforce that they are using version
+ # 2.30 or greater.
+ if (
+ parsed_args.host and
+ compute_client.api_version < api_versions.APIVersion('2.30')
+ ):
+ raise exceptions.CommandError(
+ '--os-compute-api-version 2.30 or greater is required '
+ 'when using --host'
+ )
+
+ # The host parameter is required in the API even if None.
+ kwargs['host'] = parsed_args.host
if compute_client.api_version < api_versions.APIVersion('2.25'):
kwargs['disk_over_commit'] = parsed_args.disk_overcommit
+ elif parsed_args.disk_overcommit is not None:
+ # TODO(stephenfin): Raise an error here in OSC 7.0
+ msg = _(
+ 'The --disk-overcommit and --no-disk-overcommit '
+ 'options are only supported by '
+ '--os-compute-api-version 2.24 or below; this will '
+ 'be an error in a future release'
+ )
+ self.log.warning(msg)
+
server.live_migrate(**kwargs)
- else:
+ else: # cold migration
if parsed_args.block_migration or parsed_args.disk_overcommit:
raise exceptions.CommandError(
"--live-migration must be specified if "
@@ -2503,6 +2894,70 @@ class ListMigration(command.Lister):
return self.print_migrations(parsed_args, compute_client, migrations)
+class ShowMigration(command.Command):
+ """Show a migration for a given server."""
+
+ def get_parser(self, prog_name):
+ parser = super().get_parser(prog_name)
+ parser.add_argument(
+ 'server',
+ metavar='<server>',
+ help=_('Server (name or ID)'),
+ )
+ parser.add_argument(
+ 'migration',
+ metavar='<migration>',
+ help=_("Migration (ID)"),
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ compute_client = self.app.client_manager.compute
+
+ if compute_client.api_version < api_versions.APIVersion('2.24'):
+ msg = _(
+ '--os-compute-api-version 2.24 or greater is required to '
+ 'support the server migration show command'
+ )
+ raise exceptions.CommandError(msg)
+
+ server = utils.find_resource(
+ compute_client.servers,
+ parsed_args.server,
+ )
+ server_migration = compute_client.server_migrations.get(
+ server.id, parsed_args.migration,
+ )
+
+ columns = (
+ 'ID',
+ 'Server UUID',
+ 'Status',
+ 'Source Compute',
+ 'Source Node',
+ 'Dest Compute',
+ 'Dest Host',
+ 'Dest Node',
+ 'Memory Total Bytes',
+ 'Memory Processed Bytes',
+ 'Memory Remaining Bytes',
+ 'Disk Total Bytes',
+ 'Disk Processed Bytes',
+ 'Disk Remaining Bytes',
+ 'Created At',
+ 'Updated At',
+ )
+
+ if compute_client.api_version >= api_versions.APIVersion('2.59'):
+ columns += ('UUID',)
+
+ if compute_client.api_version >= api_versions.APIVersion('2.80'):
+ columns += ('User ID', 'Project ID')
+
+ data = utils.get_item_properties(server_migration, columns)
+ return columns, data
+
+
class AbortMigration(command.Command):
"""Cancel an ongoing live migration.
@@ -3395,10 +3850,27 @@ Confirm (verify) success of resize operation and release the old server.""")
server.confirm_resize()
+# TODO(stephenfin): Remove in OSC 7.0
class MigrateConfirm(ResizeConfirm):
- _description = _("""Confirm server migrate.
+ _description = _("""DEPRECATED: Confirm server migration.
-Confirm (verify) success of migrate operation and release the old server.""")
+Use 'server migration confirm' instead.""")
+
+ def take_action(self, parsed_args):
+ msg = _(
+ "The 'server migrate confirm' command has been deprecated in "
+ "favour of the 'server migration confirm' command."
+ )
+ self.log.warning(msg)
+
+ super().take_action(parsed_args)
+
+
+class ConfirmMigration(ResizeConfirm):
+ _description = _("""Confirm server migration.
+
+Confirm (verify) success of the migration operation and release the old
+server.""")
class ResizeRevert(command.Command):
@@ -3426,10 +3898,26 @@ one.""")
server.revert_resize()
+# TODO(stephenfin): Remove in OSC 7.0
class MigrateRevert(ResizeRevert):
- _description = _("""Revert server migrate.
+ _description = _("""Revert server migration.
+
+Use 'server migration revert' instead.""")
+
+ def take_action(self, parsed_args):
+ msg = _(
+ "The 'server migrate revert' command has been deprecated in "
+ "favour of the 'server migration revert' command."
+ )
+ self.log.warning(msg)
+
+ super().take_action(parsed_args)
+
-Revert the migrate operation. Release the new server and restart the old
+class RevertMigration(ResizeRevert):
+ _description = _("""Revert server migration.
+
+Revert the migration operation. Release the new server and restart the old
one.""")
@@ -3939,6 +4427,15 @@ class StartServer(command.Command):
nargs="+",
help=_('Server(s) to start (name or ID)'),
)
+ parser.add_argument(
+ '--all-projects',
+ action='store_true',
+ default=boolenv('ALL_PROJECTS'),
+ help=_(
+ 'Start server(s) in another project by name (admin only)'
+ '(can be specified using the ALL_PROJECTS envvar)'
+ ),
+ )
return parser
def take_action(self, parsed_args):
@@ -3947,6 +4444,7 @@ class StartServer(command.Command):
utils.find_resource(
compute_client.servers,
server,
+ all_tenants=parsed_args.all_projects,
).start()
@@ -3961,6 +4459,15 @@ class StopServer(command.Command):
nargs="+",
help=_('Server(s) to stop (name or ID)'),
)
+ parser.add_argument(
+ '--all-projects',
+ action='store_true',
+ default=boolenv('ALL_PROJECTS'),
+ help=_(
+ 'Stop server(s) in another project by name (admin only)'
+ '(can be specified using the ALL_PROJECTS envvar)'
+ ),
+ )
return parser
def take_action(self, parsed_args):
@@ -3969,6 +4476,7 @@ class StopServer(command.Command):
utils.find_resource(
compute_client.servers,
server,
+ all_tenants=parsed_args.all_projects,
).stop()
diff --git a/openstackclient/compute/v2/server_event.py b/openstackclient/compute/v2/server_event.py
index 4fcc9136..1b971e51 100644
--- a/openstackclient/compute/v2/server_event.py
+++ b/openstackclient/compute/v2/server_event.py
@@ -17,7 +17,10 @@
import logging
+import iso8601
+from novaclient import api_versions
from osc_lib.command import command
+from osc_lib import exceptions
from osc_lib import utils
from openstackclient.i18n import _
@@ -27,10 +30,11 @@ LOG = logging.getLogger(__name__)
class ListServerEvent(command.Lister):
- _description = _(
- "List recent events of a server. "
- "Specify ``--os-compute-api-version 2.21`` "
- "or higher to show events for a deleted server.")
+ """List recent events of a server.
+
+ Specify ``--os-compute-api-version 2.21`` or higher to show events for a
+ deleted server.
+ """
def get_parser(self, prog_name):
parser = super(ListServerEvent, self).get_parser(prog_name)
@@ -45,60 +49,144 @@ class ListServerEvent(command.Lister):
default=False,
help=_("List additional fields in output")
)
+ parser.add_argument(
+ '--changes-since',
+ dest='changes_since',
+ metavar='<changes-since>',
+ help=_(
+ "List only server events changed later or equal to a certain "
+ "point of time. The provided time should be an ISO 8061 "
+ "formatted time, e.g. ``2016-03-04T06:27:59Z``. "
+ "(supported with --os-compute-api-version 2.58 or above)"
+ ),
+ )
+ parser.add_argument(
+ '--changes-before',
+ dest='changes_before',
+ metavar='<changes-before>',
+ help=_(
+ "List only server events changed earlier or equal to a "
+ "certain point of time. The provided time should be an ISO "
+ "8061 formatted time, e.g. ``2016-03-04T06:27:59Z``. "
+ "(supported with --os-compute-api-version 2.66 or above)"
+ ),
+ )
+ parser.add_argument(
+ '--marker',
+ help=_(
+ 'The last server event ID of the previous page '
+ '(supported by --os-compute-api-version 2.58 or above)'
+ ),
+ )
+ parser.add_argument(
+ '--limit',
+ type=int,
+ help=_(
+ 'Maximum number of server events to display '
+ '(supported by --os-compute-api-version 2.58 or above)'
+ ),
+ )
return parser
def take_action(self, parsed_args):
compute_client = self.app.client_manager.compute
- server_id = utils.find_resource(compute_client.servers,
- parsed_args.server).id
- data = compute_client.instance_action.list(server_id)
+
+ kwargs = {}
+
+ if parsed_args.marker:
+ if compute_client.api_version < api_versions.APIVersion('2.58'):
+ msg = _(
+ '--os-compute-api-version 2.58 or greater is required to '
+ 'support the --marker option'
+ )
+ raise exceptions.CommandError(msg)
+ kwargs['marker'] = parsed_args.marker
+
+ if parsed_args.limit:
+ if compute_client.api_version < api_versions.APIVersion('2.58'):
+ msg = _(
+ '--os-compute-api-version 2.58 or greater is required to '
+ 'support the --limit option'
+ )
+ raise exceptions.CommandError(msg)
+ kwargs['limit'] = parsed_args.limit
+
+ if parsed_args.changes_since:
+ if compute_client.api_version < api_versions.APIVersion('2.58'):
+ msg = _(
+ '--os-compute-api-version 2.58 or greater is required to '
+ 'support the --changes-since option'
+ )
+ raise exceptions.CommandError(msg)
+
+ try:
+ iso8601.parse_date(parsed_args.changes_since)
+ except (TypeError, iso8601.ParseError):
+ msg = _('Invalid changes-since value: %s')
+ raise exceptions.CommandError(msg % parsed_args.changes_since)
+
+ kwargs['changes_since'] = parsed_args.changes_since
+
+ if parsed_args.changes_before:
+ if compute_client.api_version < api_versions.APIVersion('2.66'):
+ msg = _(
+ '--os-compute-api-version 2.66 or greater is required to '
+ 'support the --changes-before option'
+ )
+ raise exceptions.CommandError(msg)
+
+ try:
+ iso8601.parse_date(parsed_args.changes_before)
+ except (TypeError, iso8601.ParseError):
+ msg = _('Invalid changes-before value: %s')
+ raise exceptions.CommandError(msg % parsed_args.changes_before)
+
+ kwargs['changes_before'] = parsed_args.changes_before
+
+ server_id = utils.find_resource(
+ compute_client.servers, parsed_args.server,
+ ).id
+
+ data = compute_client.instance_action.list(server_id, **kwargs)
+
+ columns = (
+ 'request_id',
+ 'instance_uuid',
+ 'action',
+ 'start_time',
+ )
+ column_headers = (
+ 'Request ID',
+ 'Server ID',
+ 'Action',
+ 'Start Time',
+ )
if parsed_args.long:
- columns = (
- 'request_id',
- 'instance_uuid',
- 'action',
- 'start_time',
+ columns += (
'message',
'project_id',
'user_id',
)
- column_headers = (
- 'Request ID',
- 'Server ID',
- 'Action',
- 'Start Time',
+ column_headers += (
'Message',
'Project ID',
'User ID',
)
- else:
- columns = (
- 'request_id',
- 'instance_uuid',
- 'action',
- 'start_time',
- )
- column_headers = (
- 'Request ID',
- 'Server ID',
- 'Action',
- 'Start Time',
- )
- return (column_headers,
- (utils.get_item_properties(
- s, columns,
- ) for s in data))
+ return (
+ column_headers,
+ (utils.get_item_properties(s, columns) for s in data),
+ )
class ShowServerEvent(command.ShowOne):
- _description = _(
- "Show server event details. "
- "Specify ``--os-compute-api-version 2.21`` "
- "or higher to show event details for a deleted server. "
- "Specify ``--os-compute-api-version 2.51`` "
- "or higher to show event details for non-admin users.")
+ """Show server event details.
+
+ Specify ``--os-compute-api-version 2.21`` or higher to show event details
+ for a deleted server. Specify ``--os-compute-api-version 2.51`` or higher
+ to show event details for non-admin users.
+ """
def get_parser(self, prog_name):
parser = super(ShowServerEvent, self).get_parser(prog_name)
@@ -116,9 +204,13 @@ class ShowServerEvent(command.ShowOne):
def take_action(self, parsed_args):
compute_client = self.app.client_manager.compute
- server_id = utils.find_resource(compute_client.servers,
- parsed_args.server).id
+
+ server_id = utils.find_resource(
+ compute_client.servers, parsed_args.server,
+ ).id
+
action_detail = compute_client.instance_action.get(
- server_id, parsed_args.request_id)
+ server_id, parsed_args.request_id
+ )
return zip(*sorted(action_detail.to_dict().items()))
diff --git a/openstackclient/compute/v2/server_group.py b/openstackclient/compute/v2/server_group.py
index 783fdbfe..32dd1937 100644
--- a/openstackclient/compute/v2/server_group.py
+++ b/openstackclient/compute/v2/server_group.py
@@ -177,11 +177,47 @@ class ListServerGroup(command.Lister):
default=False,
help=_("List additional fields in output")
)
+ # TODO(stephenfin): This should really be a --marker option, but alas
+ # the API doesn't support that for some reason
+ parser.add_argument(
+ '--offset',
+ metavar='<offset>',
+ type=int,
+ default=None,
+ help=_(
+ 'Index from which to start listing servers. This should '
+ 'typically be a factor of --limit. Display all servers groups '
+ 'if not specified.'
+ ),
+ )
+ parser.add_argument(
+ '--limit',
+ metavar='<limit>',
+ type=int,
+ default=None,
+ help=_(
+ "Maximum number of server groups to display. "
+ "If limit is greater than 'osapi_max_limit' option of Nova "
+ "API, 'osapi_max_limit' will be used instead."
+ ),
+ )
return parser
def take_action(self, parsed_args):
compute_client = self.app.client_manager.compute
- data = compute_client.server_groups.list(parsed_args.all_projects)
+
+ kwargs = {}
+
+ if parsed_args.all_projects:
+ kwargs['all_projects'] = parsed_args.all_projects
+
+ if parsed_args.offset:
+ kwargs['offset'] = parsed_args.offset
+
+ if parsed_args.limit:
+ kwargs['limit'] = parsed_args.limit
+
+ data = compute_client.server_groups.list(**kwargs)
policy_key = 'Policies'
if compute_client.api_version >= api_versions.APIVersion("2.64"):
diff --git a/openstackclient/network/v2/network_rbac.py b/openstackclient/network/v2/network_rbac.py
index b88ef019..4984e89d 100644
--- a/openstackclient/network/v2/network_rbac.py
+++ b/openstackclient/network/v2/network_rbac.py
@@ -60,6 +60,10 @@ def _get_attrs(client_manager, parsed_args):
object_id = network_client.find_subnet_pool(
parsed_args.rbac_object,
ignore_missing=False).id
+ if parsed_args.type == 'address_group':
+ object_id = network_client.find_address_group(
+ parsed_args.rbac_object,
+ ignore_missing=False).id
attrs['object_id'] = object_id
@@ -100,11 +104,12 @@ class CreateNetworkRBAC(command.ShowOne):
'--type',
metavar="<type>",
required=True,
- choices=['address_scope', 'security_group', 'subnetpool',
- 'qos_policy', 'network'],
+ choices=['address_group', 'address_scope', 'security_group',
+ 'subnetpool', 'qos_policy', 'network'],
help=_('Type of the object that RBAC policy '
- 'affects ("address_scope", "security_group", "subnetpool",'
- ' "qos_policy" or "network")')
+ 'affects ("address_group", "address_scope", '
+ '"security_group", "subnetpool", "qos_policy" or '
+ '"network")')
)
parser.add_argument(
'--action',
@@ -193,11 +198,12 @@ class ListNetworkRBAC(command.Lister):
parser.add_argument(
'--type',
metavar='<type>',
- choices=['address_scope', 'security_group', 'subnetpool',
- 'qos_policy', 'network'],
+ choices=['address_group', 'address_scope', 'security_group',
+ 'subnetpool', 'qos_policy', 'network'],
help=_('List network RBAC policies according to '
- 'given object type ("address_scope", "security_group", '
- '"subnetpool", "qos_policy" or "network")')
+ 'given object type ("address_group", "address_scope", '
+ '"security_group", "subnetpool", "qos_policy" or '
+ '"network")')
)
parser.add_argument(
'--action',
diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py
index dfdb604d..6885e147 100644
--- a/openstackclient/network/v2/port.py
+++ b/openstackclient/network/v2/port.py
@@ -600,6 +600,11 @@ class ListPort(command.Lister):
metavar='<project>',
help=_("List ports according to their project (name or ID)")
)
+ parser.add_argument(
+ '--name',
+ metavar='<name>',
+ help=_("List ports according to their name")
+ )
identity_common.add_project_domain_option_to_parser(parser)
parser.add_argument(
'--fixed-ip',
@@ -667,6 +672,8 @@ class ListPort(command.Lister):
).id
filters['tenant_id'] = project_id
filters['project_id'] = project_id
+ if parsed_args.name:
+ filters['name'] = parsed_args.name
if parsed_args.fixed_ip:
filters['fixed_ips'] = _prepare_filter_fixed_ips(
self.app.client_manager, parsed_args)
diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py
index f87f7abe..b98f8641 100644
--- a/openstackclient/network/v2/subnet.py
+++ b/openstackclient/network/v2/subnet.py
@@ -673,6 +673,11 @@ class UnsetSubnet(command.Command):
'(repeat option to unset multiple allocation pools)')
)
parser.add_argument(
+ '--gateway',
+ action='store_true',
+ help=_("Remove gateway IP from this subnet")
+ )
+ parser.add_argument(
'--dns-nameserver',
metavar='<dns-nameserver>',
action='append',
@@ -715,6 +720,8 @@ class UnsetSubnet(command.Command):
obj = client.find_subnet(parsed_args.subnet, ignore_missing=False)
attrs = {}
+ if parsed_args.gateway:
+ attrs['gateway_ip'] = None
if parsed_args.dns_nameservers:
attrs['dns_nameservers'] = copy.deepcopy(obj.dns_nameservers)
_update_arguments(attrs['dns_nameservers'],
diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py
index bad3f93d..9cf2fc7f 100644
--- a/openstackclient/tests/functional/compute/v2/test_server.py
+++ b/openstackclient/tests/functional/compute/v2/test_server.py
@@ -574,7 +574,90 @@ class ServerTests(common.ComputeTestCase):
cmd_output['status'],
)
- def test_server_boot_with_bdm_snapshot(self):
+ def _test_server_boot_with_bdm_volume(self, use_legacy):
+ """Test server create from volume, server delete"""
+ # get volume status wait function
+ volume_wait_for = volume_common.BaseVolumeTests.wait_for_status
+
+ # create source empty volume
+ volume_name = uuid.uuid4().hex
+ cmd_output = json.loads(self.openstack(
+ 'volume create -f json ' +
+ '--size 1 ' +
+ volume_name
+ ))
+ volume_id = cmd_output["id"]
+ self.assertIsNotNone(volume_id)
+ self.addCleanup(self.openstack, 'volume delete ' + volume_name)
+ self.assertEqual(volume_name, cmd_output['name'])
+ volume_wait_for("volume", volume_name, "available")
+
+ if use_legacy:
+ bdm_arg = f'--block-device-mapping vdb={volume_name}'
+ else:
+ bdm_arg = (
+ f'--block-device '
+ f'device_name=vdb,source_type=volume,boot_index=1,'
+ f'uuid={volume_id}'
+ )
+
+ # create server
+ server_name = uuid.uuid4().hex
+ server = json.loads(self.openstack(
+ 'server create -f json ' +
+ '--flavor ' + self.flavor_name + ' ' +
+ '--image ' + self.image_name + ' ' +
+ bdm_arg + ' ' +
+ self.network_arg + ' ' +
+ '--wait ' +
+ server_name
+ ))
+ self.assertIsNotNone(server["id"])
+ self.addCleanup(self.openstack, 'server delete --wait ' + server_name)
+ self.assertEqual(
+ server_name,
+ server['name'],
+ )
+
+ # check server volumes_attached, format is
+ # {"volumes_attached": "id='2518bc76-bf0b-476e-ad6b-571973745bb5'",}
+ cmd_output = json.loads(self.openstack(
+ 'server show -f json ' +
+ server_name
+ ))
+ volumes_attached = cmd_output['volumes_attached']
+ self.assertIsNotNone(volumes_attached)
+
+ # check volumes
+ cmd_output = json.loads(self.openstack(
+ 'volume show -f json ' +
+ volume_name
+ ))
+ attachments = cmd_output['attachments']
+ self.assertEqual(
+ 1,
+ len(attachments),
+ )
+ self.assertEqual(
+ server['id'],
+ attachments[0]['server_id'],
+ )
+ self.assertEqual(
+ "in-use",
+ cmd_output['status'],
+ )
+
+ def test_server_boot_with_bdm_volume(self):
+ """Test server create from image with bdm volume, server delete"""
+ self._test_server_boot_with_bdm_volume(use_legacy=False)
+
+ # TODO(stephenfin): Remove when we drop support for the
+ # '--block-device-mapping' option
+ def test_server_boot_with_bdm_volume_legacy(self):
+ """Test server create from image with bdm volume, server delete"""
+ self._test_server_boot_with_bdm_volume(use_legacy=True)
+
+ def _test_server_boot_with_bdm_snapshot(self, use_legacy):
"""Test server create from image with bdm snapshot, server delete"""
# get volume status wait function
volume_wait_for = volume_common.BaseVolumeTests.wait_for_status
@@ -588,12 +671,8 @@ class ServerTests(common.ComputeTestCase):
empty_volume_name
))
self.assertIsNotNone(cmd_output["id"])
- self.addCleanup(self.openstack,
- 'volume delete ' + empty_volume_name)
- self.assertEqual(
- empty_volume_name,
- cmd_output['name'],
- )
+ self.addCleanup(self.openstack, 'volume delete ' + empty_volume_name)
+ self.assertEqual(empty_volume_name, cmd_output['name'])
volume_wait_for("volume", empty_volume_name, "available")
# create snapshot of source empty volume
@@ -603,7 +682,8 @@ class ServerTests(common.ComputeTestCase):
'--volume ' + empty_volume_name + ' ' +
empty_snapshot_name
))
- self.assertIsNotNone(cmd_output["id"])
+ empty_snapshot_id = cmd_output["id"]
+ self.assertIsNotNone(empty_snapshot_id)
# Deleting volume snapshot take time, so we need to wait until the
# snapshot goes. Entries registered by self.addCleanup will be called
# in the reverse order, so we need to register wait_for_delete first.
@@ -617,14 +697,26 @@ class ServerTests(common.ComputeTestCase):
)
volume_wait_for("volume snapshot", empty_snapshot_name, "available")
+ if use_legacy:
+ bdm_arg = (
+ f'--block-device-mapping '
+ f'vdb={empty_snapshot_name}:snapshot:1:true'
+ )
+ else:
+ bdm_arg = (
+ f'--block-device '
+ f'device_name=vdb,uuid={empty_snapshot_id},'
+ f'source_type=snapshot,volume_size=1,'
+ f'delete_on_termination=true,boot_index=1'
+ )
+
# create server with bdm snapshot
server_name = uuid.uuid4().hex
server = json.loads(self.openstack(
'server create -f json ' +
'--flavor ' + self.flavor_name + ' ' +
'--image ' + self.image_name + ' ' +
- '--block-device-mapping '
- 'vdb=' + empty_snapshot_name + ':snapshot:1:true ' +
+ bdm_arg + ' ' +
self.network_arg + ' ' +
'--wait ' +
server_name
@@ -681,7 +773,17 @@ class ServerTests(common.ComputeTestCase):
# the attached volume had been deleted
pass
- def test_server_boot_with_bdm_image(self):
+ def test_server_boot_with_bdm_snapshot(self):
+ """Test server create from image with bdm snapshot, server delete"""
+ self._test_server_boot_with_bdm_snapshot(use_legacy=False)
+
+ # TODO(stephenfin): Remove when we drop support for the
+ # '--block-device-mapping' option
+ def test_server_boot_with_bdm_snapshot_legacy(self):
+ """Test server create from image with bdm snapshot, server delete"""
+ self._test_server_boot_with_bdm_snapshot(use_legacy=True)
+
+ def _test_server_boot_with_bdm_image(self, use_legacy):
# Tests creating a server where the root disk is backed by the given
# --image but a --block-device-mapping with type=image is provided so
# that the compute service creates a volume from that image and
@@ -689,6 +791,32 @@ class ServerTests(common.ComputeTestCase):
# marked as delete_on_termination=True so it will be automatically
# deleted when the server is deleted.
+ if use_legacy:
+ # This means create a 1GB volume from the specified image, attach
+ # it to the server at /dev/vdb and delete the volume when the
+ # server is deleted.
+ bdm_arg = (
+ f'--block-device-mapping '
+ f'vdb={self.image_name}:image:1:true '
+ )
+ else:
+ # get image ID
+ cmd_output = json.loads(self.openstack(
+ 'image show -f json ' +
+ self.image_name
+ ))
+ image_id = cmd_output['id']
+
+ # This means create a 1GB volume from the specified image, attach
+ # it to the server at /dev/vdb and delete the volume when the
+ # server is deleted.
+ bdm_arg = (
+ f'--block-device '
+ f'device_name=vdb,uuid={image_id},'
+ f'source_type=image,volume_size=1,'
+ f'delete_on_termination=true,boot_index=1'
+ )
+
# create server with bdm type=image
# NOTE(mriedem): This test is a bit unrealistic in that specifying the
# same image in the block device as the --image option does not really
@@ -700,11 +828,7 @@ class ServerTests(common.ComputeTestCase):
'server create -f json ' +
'--flavor ' + self.flavor_name + ' ' +
'--image ' + self.image_name + ' ' +
- '--block-device-mapping '
- # This means create a 1GB volume from the specified image, attach
- # it to the server at /dev/vdb and delete the volume when the
- # server is deleted.
- 'vdb=' + self.image_name + ':image:1:true ' +
+ bdm_arg + ' ' +
self.network_arg + ' ' +
'--wait ' +
server_name
@@ -768,6 +892,14 @@ class ServerTests(common.ComputeTestCase):
# the attached volume had been deleted
pass
+ def test_server_boot_with_bdm_image(self):
+ self._test_server_boot_with_bdm_image(use_legacy=False)
+
+ # TODO(stephenfin): Remove when we drop support for the
+ # '--block-device-mapping' option
+ def test_server_boot_with_bdm_image_legacy(self):
+ self._test_server_boot_with_bdm_image(use_legacy=True)
+
def test_boot_from_volume(self):
# Tests creating a server using --image and --boot-from-volume where
# the compute service will create a root volume of the specified size
diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py
index e4cf1045..4a2a44de 100644
--- a/openstackclient/tests/unit/compute/v2/fakes.py
+++ b/openstackclient/tests/unit/compute/v2/fakes.py
@@ -1566,12 +1566,12 @@ class FakeRateLimit(object):
self.next_available = next_available
-class FakeServerMigration(object):
- """Fake one or more server migrations."""
+class FakeMigration(object):
+ """Fake one or more migrations."""
@staticmethod
- def create_one_server_migration(attrs=None, methods=None):
- """Create a fake server migration.
+ def create_one_migration(attrs=None, methods=None):
+ """Create a fake migration.
:param Dictionary attrs:
A dictionary with all attributes
@@ -1612,27 +1612,80 @@ class FakeServerMigration(object):
return migration
@staticmethod
- def create_server_migrations(attrs=None, methods=None, count=2):
- """Create multiple fake server migrations.
+ def create_migrations(attrs=None, methods=None, count=2):
+ """Create multiple fake migrations.
:param Dictionary attrs:
A dictionary with all attributes
:param Dictionary methods:
A dictionary with all methods
:param int count:
- The number of server migrations to fake
+ The number of migrations to fake
:return:
- A list of FakeResource objects faking the server migrations
+ A list of FakeResource objects faking the migrations
"""
migrations = []
for i in range(0, count):
migrations.append(
- FakeServerMigration.create_one_server_migration(
+ FakeMigration.create_one_migration(
attrs, methods))
return migrations
+class FakeServerMigration(object):
+ """Fake one or more server migrations."""
+
+ @staticmethod
+ def create_one_server_migration(attrs=None, methods=None):
+ """Create a fake server migration.
+
+ :param Dictionary attrs:
+ A dictionary with all attributes
+ :param Dictionary methods:
+ A dictionary with all methods
+ :return:
+ A FakeResource object, with id, type, and so on
+ """
+ attrs = attrs or {}
+ methods = methods or {}
+
+ # Set default attributes.
+
+ migration_info = {
+ "created_at": "2016-01-29T13:42:02.000000",
+ "dest_compute": "compute2",
+ "dest_host": "1.2.3.4",
+ "dest_node": "node2",
+ "id": random.randint(1, 999),
+ "server_uuid": uuid.uuid4().hex,
+ "source_compute": "compute1",
+ "source_node": "node1",
+ "status": "running",
+ "memory_total_bytes": random.randint(1, 99999),
+ "memory_processed_bytes": random.randint(1, 99999),
+ "memory_remaining_bytes": random.randint(1, 99999),
+ "disk_total_bytes": random.randint(1, 99999),
+ "disk_processed_bytes": random.randint(1, 99999),
+ "disk_remaining_bytes": random.randint(1, 99999),
+ "updated_at": "2016-01-29T13:42:02.000000",
+ # added in 2.59
+ "uuid": uuid.uuid4().hex,
+ # added in 2.80
+ "user_id": uuid.uuid4().hex,
+ "project_id": uuid.uuid4().hex,
+ }
+
+ # Overwrite default attributes.
+ migration_info.update(attrs)
+
+ migration = fakes.FakeResource(
+ info=copy.deepcopy(migration_info),
+ methods=methods,
+ loaded=True)
+ return migration
+
+
class FakeVolumeAttachment(object):
"""Fake one or more volume attachments (BDMs)."""
@@ -1680,7 +1733,7 @@ class FakeVolumeAttachment(object):
:param Dictionary methods:
A dictionary with all methods
:param int count:
- The number of server migrations to fake
+ The number of volume attachments to fake
:return:
A list of FakeResource objects faking the volume attachments.
"""
diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py
index 16885eb8..c6dff5a8 100644
--- a/openstackclient/tests/unit/compute/v2/test_server.py
+++ b/openstackclient/tests/unit/compute/v2/test_server.py
@@ -16,6 +16,8 @@ import argparse
import collections
import copy
import getpass
+import json
+import tempfile
from unittest import mock
from unittest.mock import call
@@ -1311,8 +1313,28 @@ class TestServerCreate(TestServer):
verifylist = [
('image', 'image1'),
('flavor', 'flavor1'),
- ('nic', ['net-id=net1', 'net-id=net1,v4-fixed-ip=10.0.0.2',
- 'port-id=port1', 'net-id=net1', 'port-id=port2']),
+ ('nics', [
+ {
+ 'net-id': 'net1', 'port-id': '',
+ 'v4-fixed-ip': '', 'v6-fixed-ip': '',
+ },
+ {
+ 'net-id': 'net1', 'port-id': '',
+ 'v4-fixed-ip': '10.0.0.2', 'v6-fixed-ip': '',
+ },
+ {
+ 'net-id': '', 'port-id': 'port1',
+ 'v4-fixed-ip': '', 'v6-fixed-ip': '',
+ },
+ {
+ 'net-id': 'net1', 'port-id': '',
+ 'v4-fixed-ip': '', 'v6-fixed-ip': '',
+ },
+ {
+ 'net-id': '', 'port-id': 'port2',
+ 'v4-fixed-ip': '', 'v6-fixed-ip': '',
+ },
+ ]),
('config_drive', False),
('server_name', self.new_server.name),
]
@@ -1403,6 +1425,113 @@ class TestServerCreate(TestServer):
self.assertEqual(self.columns, columns)
self.assertEqual(self.datalist(), data)
+ def test_server_create_with_network_tag(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.43')
+
+ arglist = [
+ '--image', 'image1',
+ '--flavor', 'flavor1',
+ '--nic', 'net-id=net1,tag=foo',
+ self.new_server.name,
+ ]
+ verifylist = [
+ ('image', 'image1'),
+ ('flavor', 'flavor1'),
+ ('nics', [
+ {
+ 'net-id': 'net1', 'port-id': '',
+ 'v4-fixed-ip': '', 'v6-fixed-ip': '',
+ 'tag': 'foo',
+ },
+ ]),
+ ('server_name', self.new_server.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ find_network = mock.Mock()
+ network_client = self.app.client_manager.network
+ network_client.find_network = find_network
+ network_resource = mock.Mock(id='net1_uuid')
+ find_network.return_value = network_resource
+
+ # Mock sdk APIs.
+ _network = mock.Mock(id='net1_uuid')
+ find_network = mock.Mock()
+ find_network.return_value = _network
+ self.app.client_manager.network.find_network = find_network
+
+ # In base command class ShowOne in cliff, abstract method take_action()
+ # returns a two-part tuple with a tuple of column names and a tuple of
+ # data to be shown.
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = dict(
+ 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=[],
+ nics=[
+ {
+ 'net-id': 'net1_uuid',
+ 'v4-fixed-ip': '',
+ 'v6-fixed-ip': '',
+ 'port-id': '',
+ 'tag': 'foo',
+ },
+ ],
+ 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)
+
+ network_client.find_network.assert_called_once()
+ self.app.client_manager.network.find_network.assert_called_once()
+
+ def test_server_create_with_network_tag_pre_v243(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.42')
+
+ arglist = [
+ '--image', 'image1',
+ '--flavor', 'flavor1',
+ '--nic', 'net-id=net1,tag=foo',
+ self.new_server.name,
+ ]
+ verifylist = [
+ ('image', 'image1'),
+ ('flavor', 'flavor1'),
+ ('nics', [
+ {
+ 'net-id': 'net1', 'port-id': '',
+ 'v4-fixed-ip': '', 'v6-fixed-ip': '',
+ 'tag': 'foo',
+ },
+ ]),
+ ('server_name', self.new_server.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ self.assertRaises(
+ exceptions.CommandError, self.cmd.take_action, parsed_args)
+
def test_server_create_with_auto_network(self):
arglist = [
'--image', 'image1',
@@ -1413,7 +1542,7 @@ class TestServerCreate(TestServer):
verifylist = [
('image', 'image1'),
('flavor', 'flavor1'),
- ('nic', ['auto']),
+ ('nics', ['auto']),
('config_drive', False),
('server_name', self.new_server.name),
]
@@ -1509,7 +1638,7 @@ class TestServerCreate(TestServer):
verifylist = [
('image', 'image1'),
('flavor', 'flavor1'),
- ('nic', ['none']),
+ ('nics', ['none']),
('config_drive', False),
('server_name', self.new_server.name),
]
@@ -1557,7 +1686,14 @@ class TestServerCreate(TestServer):
verifylist = [
('image', 'image1'),
('flavor', 'flavor1'),
- ('nic', ['none', 'auto', 'port-id=port1']),
+ ('nics', [
+ 'none',
+ 'auto',
+ {
+ 'net-id': '', 'port-id': 'port1',
+ 'v4-fixed-ip': '', 'v6-fixed-ip': '',
+ },
+ ]),
('config_drive', False),
('server_name', self.new_server.name),
]
@@ -1587,17 +1723,10 @@ class TestServerCreate(TestServer):
'--nic', 'abcdefgh',
self.new_server.name,
]
- verifylist = [
- ('image', 'image1'),
- ('flavor', 'flavor1'),
- ('nic', ['abcdefgh']),
- ('config_drive', False),
- ('server_name', self.new_server.name),
- ]
- parsed_args = self.check_parser(self.cmd, arglist, verifylist)
-
- self.assertRaises(exceptions.CommandError,
- self.cmd.take_action, parsed_args)
+ self.assertRaises(
+ argparse.ArgumentTypeError,
+ self.check_parser,
+ self.cmd, arglist, [])
self.assertNotCalled(self.servers_mock.create)
def test_server_create_with_invalid_network_key(self):
@@ -1607,17 +1736,10 @@ class TestServerCreate(TestServer):
'--nic', 'abcdefgh=12324',
self.new_server.name,
]
- verifylist = [
- ('image', 'image1'),
- ('flavor', 'flavor1'),
- ('nic', ['abcdefgh=12324']),
- ('config_drive', False),
- ('server_name', self.new_server.name),
- ]
- parsed_args = self.check_parser(self.cmd, arglist, verifylist)
-
- self.assertRaises(exceptions.CommandError,
- self.cmd.take_action, parsed_args)
+ self.assertRaises(
+ argparse.ArgumentTypeError,
+ self.check_parser,
+ self.cmd, arglist, [])
self.assertNotCalled(self.servers_mock.create)
def test_server_create_with_empty_network_key_value(self):
@@ -1627,17 +1749,10 @@ class TestServerCreate(TestServer):
'--nic', 'net-id=',
self.new_server.name,
]
- verifylist = [
- ('image', 'image1'),
- ('flavor', 'flavor1'),
- ('nic', ['net-id=']),
- ('config_drive', False),
- ('server_name', self.new_server.name),
- ]
- parsed_args = self.check_parser(self.cmd, arglist, verifylist)
-
- self.assertRaises(exceptions.CommandError,
- self.cmd.take_action, parsed_args)
+ self.assertRaises(
+ argparse.ArgumentTypeError,
+ self.check_parser,
+ self.cmd, arglist, [])
self.assertNotCalled(self.servers_mock.create)
def test_server_create_with_only_network_key(self):
@@ -1647,17 +1762,11 @@ class TestServerCreate(TestServer):
'--nic', 'net-id',
self.new_server.name,
]
- verifylist = [
- ('image', 'image1'),
- ('flavor', 'flavor1'),
- ('nic', ['net-id']),
- ('config_drive', False),
- ('server_name', self.new_server.name),
- ]
- parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ self.assertRaises(
+ argparse.ArgumentTypeError,
+ self.check_parser,
+ self.cmd, arglist, [])
- self.assertRaises(exceptions.CommandError,
- self.cmd.take_action, parsed_args)
self.assertNotCalled(self.servers_mock.create)
@mock.patch.object(common_utils, 'wait_for_status', return_value=True)
@@ -1818,6 +1927,430 @@ 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(self):
+ block_device = f'uuid={self.volume.id},source_type=volume'
+ arglist = [
+ '--image', 'image1',
+ '--flavor', self.flavor.id,
+ '--block-device', block_device,
+ self.new_server.name,
+ ]
+ verifylist = [
+ ('image', 'image1'),
+ ('flavor', self.flavor.id),
+ ('block_devices', [
+ {
+ 'uuid': self.volume.id,
+ 'source_type': 'volume',
+ },
+ ]),
+ ('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,
+ '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,
+ self.image,
+ self.flavor,
+ **kwargs
+ )
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(self.datalist(), data)
+
+ def test_server_create_with_block_device_full(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.67')
+
+ block_device = (
+ f'uuid={self.volume.id},source_type=volume,'
+ f'destination_type=volume,disk_bus=ide,device_type=disk,'
+ f'device_name=sdb,guest_format=ext4,volume_size=64,'
+ f'volume_type=foo,boot_index=1,delete_on_termination=true,'
+ f'tag=foo'
+ )
+
+ arglist = [
+ '--image', 'image1',
+ '--flavor', self.flavor.id,
+ '--block-device', block_device,
+ self.new_server.name,
+ ]
+ verifylist = [
+ ('image', 'image1'),
+ ('flavor', self.flavor.id),
+ ('block_devices', [
+ {
+ 'uuid': self.volume.id,
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'disk_bus': 'ide',
+ 'device_type': 'disk',
+ 'device_name': 'sdb',
+ 'guest_format': 'ext4',
+ 'volume_size': '64',
+ 'volume_type': 'foo',
+ 'boot_index': '1',
+ 'delete_on_termination': 'true',
+ 'tag': 'foo',
+ },
+ ]),
+ ('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,
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'disk_bus': 'ide',
+ 'device_name': 'sdb',
+ 'volume_size': '64',
+ 'guest_format': 'ext4',
+ 'boot_index': 1,
+ 'device_type': 'disk',
+ 'delete_on_termination': True,
+ 'tag': 'foo',
+ 'volume_type': 'foo',
+ }],
+ 'nics': 'auto',
+ '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_block_device_from_file(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.67')
+
+ block_device = {
+ 'uuid': self.volume.id,
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'disk_bus': 'ide',
+ 'device_type': 'disk',
+ 'device_name': 'sdb',
+ 'guest_format': 'ext4',
+ 'volume_size': 64,
+ 'volume_type': 'foo',
+ 'boot_index': 1,
+ 'delete_on_termination': True,
+ 'tag': 'foo',
+ }
+
+ with tempfile.NamedTemporaryFile(mode='w+') as fp:
+ json.dump(block_device, fp=fp)
+ fp.flush()
+
+ arglist = [
+ '--image', 'image1',
+ '--flavor', self.flavor.id,
+ '--block-device', fp.name,
+ self.new_server.name,
+ ]
+ verifylist = [
+ ('image', 'image1'),
+ ('flavor', self.flavor.id),
+ ('block_devices', [block_device]),
+ ('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,
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'disk_bus': 'ide',
+ 'device_name': 'sdb',
+ 'volume_size': 64,
+ 'guest_format': 'ext4',
+ 'boot_index': 1,
+ 'device_type': 'disk',
+ 'delete_on_termination': True,
+ 'tag': 'foo',
+ 'volume_type': 'foo',
+ }],
+ 'nics': 'auto',
+ '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_block_device_invalid_boot_index(self):
+ block_device = \
+ f'uuid={self.volume.name},source_type=volume,boot_index=foo'
+ arglist = [
+ '--image', 'image1',
+ '--flavor', self.flavor.id,
+ '--block-device', block_device,
+ self.new_server.name,
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action, parsed_args)
+ self.assertIn('The boot_index key of --block-device ', str(ex))
+
+ def test_server_create_with_block_device_invalid_source_type(self):
+ block_device = f'uuid={self.volume.name},source_type=foo'
+ arglist = [
+ '--image', 'image1',
+ '--flavor', self.flavor.id,
+ '--block-device', block_device,
+ self.new_server.name,
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action, parsed_args)
+ self.assertIn('The source_type key of --block-device ', str(ex))
+
+ def test_server_create_with_block_device_invalid_destination_type(self):
+ block_device = \
+ f'uuid={self.volume.name},destination_type=foo'
+ arglist = [
+ '--image', 'image1',
+ '--flavor', self.flavor.id,
+ '--block-device', block_device,
+ self.new_server.name,
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action, parsed_args)
+ self.assertIn('The destination_type key of --block-device ', str(ex))
+
+ def test_server_create_with_block_device_invalid_shutdown(self):
+ block_device = \
+ f'uuid={self.volume.name},delete_on_termination=foo'
+ arglist = [
+ '--image', 'image1',
+ '--flavor', self.flavor.id,
+ '--block-device', block_device,
+ self.new_server.name,
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action, parsed_args)
+ self.assertIn(
+ 'The delete_on_termination key of --block-device ', str(ex))
+
+ def test_server_create_with_block_device_tag_pre_v242(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.41')
+
+ block_device = \
+ f'uuid={self.volume.name},tag=foo'
+ arglist = [
+ '--image', 'image1',
+ '--flavor', self.flavor.id,
+ '--block-device', block_device,
+ self.new_server.name,
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action, parsed_args)
+ self.assertIn(
+ '--os-compute-api-version 2.42 or greater is required',
+ str(ex))
+
+ def test_server_create_with_block_device_volume_type_pre_v267(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.66')
+
+ block_device = f'uuid={self.volume.name},volume_type=foo'
+ arglist = [
+ '--image', 'image1',
+ '--flavor', self.flavor.id,
+ '--block-device', block_device,
+ self.new_server.name,
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action, parsed_args)
+ self.assertIn(
+ '--os-compute-api-version 2.67 or greater is required',
+ str(ex))
+
def test_server_create_with_block_device_mapping(self):
arglist = [
'--image', 'image1',
@@ -1828,7 +2361,15 @@ class TestServerCreate(TestServer):
verifylist = [
('image', 'image1'),
('flavor', self.flavor.id),
- ('block_device_mapping', {'vda': self.volume.name + ':::false'}),
+ ('block_device_mapping', [
+ {
+ 'device_name': 'vda',
+ 'uuid': self.volume.name,
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'delete_on_termination': 'false',
+ }
+ ]),
('config_drive', False),
('server_name', self.new_server.name),
]
@@ -1881,7 +2422,14 @@ class TestServerCreate(TestServer):
verifylist = [
('image', 'image1'),
('flavor', self.flavor.id),
- ('block_device_mapping', {'vdf': self.volume.name}),
+ ('block_device_mapping', [
+ {
+ 'device_name': 'vdf',
+ 'uuid': self.volume.name,
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ }
+ ]),
('config_drive', False),
('server_name', self.new_server.name),
]
@@ -1933,7 +2481,14 @@ class TestServerCreate(TestServer):
verifylist = [
('image', 'image1'),
('flavor', self.flavor.id),
- ('block_device_mapping', {'vdf': self.volume.name + ':::'}),
+ ('block_device_mapping', [
+ {
+ 'device_name': 'vdf',
+ 'uuid': self.volume.name,
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ }
+ ]),
('config_drive', False),
('server_name', self.new_server.name),
]
@@ -1986,8 +2541,16 @@ class TestServerCreate(TestServer):
verifylist = [
('image', 'image1'),
('flavor', self.flavor.id),
- ('block_device_mapping',
- {'vde': self.volume.name + ':volume:3:true'}),
+ ('block_device_mapping', [
+ {
+ 'device_name': 'vde',
+ 'uuid': self.volume.name,
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'volume_size': '3',
+ 'delete_on_termination': 'true',
+ }
+ ]),
('config_drive', False),
('server_name', self.new_server.name),
]
@@ -2042,8 +2605,16 @@ class TestServerCreate(TestServer):
verifylist = [
('image', 'image1'),
('flavor', self.flavor.id),
- ('block_device_mapping',
- {'vds': self.volume.name + ':snapshot:5:true'}),
+ ('block_device_mapping', [
+ {
+ 'device_name': 'vds',
+ 'uuid': self.volume.name,
+ 'source_type': 'snapshot',
+ 'volume_size': '5',
+ 'destination_type': 'volume',
+ 'delete_on_termination': 'true',
+ }
+ ]),
('config_drive', False),
('server_name', self.new_server.name),
]
@@ -2098,8 +2669,22 @@ class TestServerCreate(TestServer):
verifylist = [
('image', 'image1'),
('flavor', self.flavor.id),
- ('block_device_mapping', {'vdb': self.volume.name + ':::false',
- 'vdc': self.volume.name + ':::true'}),
+ ('block_device_mapping', [
+ {
+ 'device_name': 'vdb',
+ 'uuid': self.volume.name,
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'delete_on_termination': 'false',
+ },
+ {
+ 'device_name': 'vdc',
+ 'uuid': self.volume.name,
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'delete_on_termination': 'true',
+ },
+ ]),
('config_drive', False),
('server_name', self.new_server.name),
]
@@ -2152,26 +2737,29 @@ class TestServerCreate(TestServer):
self.assertEqual(self.datalist(), data)
def test_server_create_with_block_device_mapping_invalid_format(self):
- # 1. block device mapping don't contain equal sign "="
+ # block device mapping don't contain equal sign "="
arglist = [
'--image', 'image1',
'--flavor', self.flavor.id,
'--block-device-mapping', 'not_contain_equal_sign',
self.new_server.name,
]
- self.assertRaises(argparse.ArgumentTypeError,
- self.check_parser,
- self.cmd, arglist, [])
- # 2. block device mapping don't contain device name "=uuid:::true"
+ self.assertRaises(
+ argparse.ArgumentTypeError,
+ self.check_parser,
+ self.cmd, arglist, [])
+
+ # block device mapping don't contain device name "=uuid:::true"
arglist = [
'--image', 'image1',
'--flavor', self.flavor.id,
'--block-device-mapping', '=uuid:::true',
self.new_server.name,
]
- self.assertRaises(argparse.ArgumentTypeError,
- self.check_parser,
- self.cmd, arglist, [])
+ self.assertRaises(
+ argparse.ArgumentTypeError,
+ self.check_parser,
+ self.cmd, arglist, [])
def test_server_create_with_block_device_mapping_no_uuid(self):
arglist = [
@@ -2180,18 +2768,10 @@ class TestServerCreate(TestServer):
'--block-device-mapping', 'vdb=',
self.new_server.name,
]
- verifylist = [
- ('image', 'image1'),
- ('flavor', self.flavor.id),
- ('block_device_mapping', {'vdb': ''}),
- ('config_drive', False),
- ('server_name', self.new_server.name),
- ]
- parsed_args = self.check_parser(self.cmd, arglist, verifylist)
-
- self.assertRaises(exceptions.CommandError,
- self.cmd.take_action,
- parsed_args)
+ self.assertRaises(
+ argparse.ArgumentTypeError,
+ self.check_parser,
+ self.cmd, arglist, [])
def test_server_create_volume_boot_from_volume_conflict(self):
# Tests that specifying --volume and --boot-from-volume results in
@@ -2230,7 +2810,7 @@ class TestServerCreate(TestServer):
verifylist = [
('image_properties', {'hypervisor_type': 'qemu'}),
('flavor', 'flavor1'),
- ('nic', ['none']),
+ ('nics', ['none']),
('config_drive', False),
('server_name', self.new_server.name),
]
@@ -2286,7 +2866,7 @@ class TestServerCreate(TestServer):
('image_properties', {'hypervisor_type': 'qemu',
'hw_disk_bus': 'ide'}),
('flavor', 'flavor1'),
- ('nic', ['none']),
+ ('nics', ['none']),
('config_drive', False),
('server_name', self.new_server.name),
]
@@ -2342,7 +2922,7 @@ class TestServerCreate(TestServer):
('image_properties', {'hypervisor_type': 'qemu',
'hw_disk_bus': 'virtio'}),
('flavor', 'flavor1'),
- ('nic', ['none']),
+ ('nics', ['none']),
('config_drive', False),
('server_name', self.new_server.name),
]
@@ -2374,7 +2954,7 @@ class TestServerCreate(TestServer):
('image_properties',
{'owner_specified.openstack.object': 'image/cirros'}),
('flavor', 'flavor1'),
- ('nic', ['none']),
+ ('nics', ['none']),
('server_name', self.new_server.name),
]
# create a image_info as the side_effect of the fake image_list()
@@ -2421,6 +3001,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 = [
@@ -2913,6 +3623,28 @@ class TestServerDelete(TestServer):
self.servers_mock.delete.assert_has_calls(calls)
self.assertIsNone(result)
+ @mock.patch.object(common_utils, 'find_resource')
+ def test_server_delete_with_all_projects(self, mock_find_resource):
+ servers = self.setup_servers_mock(count=1)
+ mock_find_resource.side_effect = compute_fakes.FakeServer.get_servers(
+ servers, 0,
+ )
+
+ arglist = [
+ servers[0].id,
+ '--all-projects',
+ ]
+ verifylist = [
+ ('server', [servers[0].id]),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ self.cmd.take_action(parsed_args)
+
+ mock_find_resource.assert_called_once_with(
+ mock.ANY, servers[0].id, all_tenants=True,
+ )
+
@mock.patch.object(common_utils, 'wait_for_delete', return_value=True)
def test_server_delete_wait_ok(self, mock_wait_for_delete):
servers = self.setup_servers_mock(count=1)
@@ -3858,8 +4590,8 @@ class TestServerMigrate(TestServer):
self.server.id,
]
verifylist = [
- ('live', None),
- ('block_migration', False),
+ ('live_migration', False),
+ ('block_migration', None),
('disk_overcommit', False),
('wait', False),
]
@@ -3879,10 +4611,9 @@ class TestServerMigrate(TestServer):
'--host', 'fakehost', self.server.id,
]
verifylist = [
- ('live', None),
('live_migration', False),
('host', 'fakehost'),
- ('block_migration', False),
+ ('block_migration', None),
('disk_overcommit', False),
('wait', False),
]
@@ -3903,7 +4634,7 @@ class TestServerMigrate(TestServer):
'--block-migration', self.server.id,
]
verifylist = [
- ('live', None),
+ ('live_migration', False),
('block_migration', True),
('disk_overcommit', False),
('wait', False),
@@ -3922,8 +4653,8 @@ class TestServerMigrate(TestServer):
'--disk-overcommit', self.server.id,
]
verifylist = [
- ('live', None),
- ('block_migration', False),
+ ('live_migration', False),
+ ('block_migration', None),
('disk_overcommit', True),
('wait', False),
]
@@ -3943,10 +4674,9 @@ class TestServerMigrate(TestServer):
'--host', 'fakehost', self.server.id,
]
verifylist = [
- ('live', None),
('live_migration', False),
('host', 'fakehost'),
- ('block_migration', False),
+ ('block_migration', None),
('disk_overcommit', False),
('wait', False),
]
@@ -3965,101 +4695,38 @@ class TestServerMigrate(TestServer):
self.assertNotCalled(self.servers_mock.migrate)
def test_server_live_migrate(self):
- arglist = [
- '--live', 'fakehost', self.server.id,
- ]
- verifylist = [
- ('live', 'fakehost'),
- ('live_migration', False),
- ('host', None),
- ('block_migration', False),
- ('disk_overcommit', False),
- ('wait', False),
- ]
- parsed_args = self.check_parser(self.cmd, arglist, verifylist)
-
- self.app.client_manager.compute.api_version = \
- api_versions.APIVersion('2.24')
-
- with mock.patch.object(self.cmd.log, 'warning') as mock_warning:
- result = self.cmd.take_action(parsed_args)
-
- self.servers_mock.get.assert_called_with(self.server.id)
- self.server.live_migrate.assert_called_with(block_migration=False,
- disk_over_commit=False,
- host='fakehost')
- self.assertNotCalled(self.servers_mock.migrate)
- self.assertIsNone(result)
- # A warning should have been logged for using --live.
- mock_warning.assert_called_once()
- self.assertIn('The --live option has been deprecated.',
- str(mock_warning.call_args[0][0]))
-
- def test_server_live_migrate_host_pre_2_30(self):
- # Tests that the --host option is not supported for --live-migration
- # before microversion 2.30 (the test defaults to 2.1).
- arglist = [
- '--live-migration', '--host', 'fakehost', self.server.id,
- ]
- verifylist = [
- ('live', None),
- ('live_migration', True),
- ('host', 'fakehost'),
- ('block_migration', False),
- ('disk_overcommit', False),
- ('wait', False),
- ]
- parsed_args = self.check_parser(self.cmd, arglist, verifylist)
-
- ex = self.assertRaises(exceptions.CommandError, self.cmd.take_action,
- parsed_args)
-
- # Make sure it's the error we expect.
- self.assertIn('--os-compute-api-version 2.30 or greater is required '
- 'when using --host', str(ex))
-
- self.servers_mock.get.assert_called_with(self.server.id)
- self.assertNotCalled(self.servers_mock.live_migrate)
- self.assertNotCalled(self.servers_mock.migrate)
-
- def test_server_live_migrate_no_host(self):
# Tests the --live-migration option without --host or --live.
arglist = [
'--live-migration', self.server.id,
]
verifylist = [
- ('live', None),
('live_migration', True),
('host', None),
- ('block_migration', False),
+ ('block_migration', None),
('disk_overcommit', False),
('wait', False),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
- with mock.patch.object(self.cmd.log, 'warning') as mock_warning:
- result = self.cmd.take_action(parsed_args)
+ result = self.cmd.take_action(parsed_args)
self.servers_mock.get.assert_called_with(self.server.id)
- self.server.live_migrate.assert_called_with(block_migration=False,
- disk_over_commit=False,
- host=None)
+ self.server.live_migrate.assert_called_with(
+ block_migration=False,
+ disk_over_commit=False,
+ host=None)
self.assertNotCalled(self.servers_mock.migrate)
self.assertIsNone(result)
- # Since --live wasn't used a warning shouldn't have been logged.
- mock_warning.assert_not_called()
def test_server_live_migrate_with_host(self):
- # Tests the --live-migration option with --host but no --live.
# This requires --os-compute-api-version >= 2.30 so the test uses 2.30.
arglist = [
'--live-migration', '--host', 'fakehost', self.server.id,
]
verifylist = [
- ('live', None),
('live_migration', True),
('host', 'fakehost'),
- ('block_migration', False),
+ ('block_migration', None),
('disk_overcommit', False),
('wait', False),
]
@@ -4071,58 +4738,49 @@ class TestServerMigrate(TestServer):
result = self.cmd.take_action(parsed_args)
self.servers_mock.get.assert_called_with(self.server.id)
- # No disk_overcommit with microversion >= 2.25.
- self.server.live_migrate.assert_called_with(block_migration=False,
- host='fakehost')
+ # No disk_overcommit and block_migration defaults to auto with
+ # microversion >= 2.25
+ self.server.live_migrate.assert_called_with(
+ block_migration='auto', host='fakehost')
self.assertNotCalled(self.servers_mock.migrate)
self.assertIsNone(result)
- def test_server_live_migrate_without_host_override_live(self):
- # Tests the --live-migration option without --host and with --live.
- # The --live-migration option will take precedence and a warning is
- # logged for using --live.
+ def test_server_live_migrate_with_host_pre_v230(self):
+ # Tests that the --host option is not supported for --live-migration
+ # before microversion 2.30 (the test defaults to 2.1).
arglist = [
- '--live', 'fakehost', '--live-migration', self.server.id,
+ '--live-migration', '--host', 'fakehost', self.server.id,
]
verifylist = [
- ('live', 'fakehost'),
('live_migration', True),
- ('host', None),
- ('block_migration', False),
+ ('host', 'fakehost'),
+ ('block_migration', None),
('disk_overcommit', False),
('wait', False),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
- with mock.patch.object(self.cmd.log, 'warning') as mock_warning:
- result = self.cmd.take_action(parsed_args)
+ ex = self.assertRaises(
+ exceptions.CommandError, self.cmd.take_action,
+ parsed_args)
+
+ # Make sure it's the error we expect.
+ self.assertIn(
+ '--os-compute-api-version 2.30 or greater is required '
+ 'when using --host', str(ex))
self.servers_mock.get.assert_called_with(self.server.id)
- self.server.live_migrate.assert_called_with(block_migration=False,
- disk_over_commit=False,
- host=None)
+ self.assertNotCalled(self.servers_mock.live_migrate)
self.assertNotCalled(self.servers_mock.migrate)
- self.assertIsNone(result)
- # A warning should have been logged for using --live.
- mock_warning.assert_called_once()
- self.assertIn('The --live option has been deprecated.',
- str(mock_warning.call_args[0][0]))
-
- def test_server_live_migrate_live_and_host_mutex(self):
- # Tests specifying both the --live and --host options which are in a
- # mutex group so argparse should fail.
- arglist = [
- '--live', 'fakehost', '--host', 'fakehost', self.server.id,
- ]
- self.assertRaises(utils.ParserException,
- self.check_parser, self.cmd, arglist, verify_args=[])
def test_server_block_live_migrate(self):
arglist = [
- '--live', 'fakehost', '--block-migration', self.server.id,
+ '--live-migration',
+ '--block-migration',
+ self.server.id,
]
verifylist = [
- ('live', 'fakehost'),
+ ('live_migration', True),
('block_migration', True),
('disk_overcommit', False),
('wait', False),
@@ -4135,19 +4793,22 @@ class TestServerMigrate(TestServer):
result = self.cmd.take_action(parsed_args)
self.servers_mock.get.assert_called_with(self.server.id)
- self.server.live_migrate.assert_called_with(block_migration=True,
- disk_over_commit=False,
- host='fakehost')
+ self.server.live_migrate.assert_called_with(
+ block_migration=True,
+ disk_over_commit=False,
+ host=None)
self.assertNotCalled(self.servers_mock.migrate)
self.assertIsNone(result)
def test_server_live_migrate_with_disk_overcommit(self):
arglist = [
- '--live', 'fakehost', '--disk-overcommit', self.server.id,
+ '--live-migration',
+ '--disk-overcommit',
+ self.server.id,
]
verifylist = [
- ('live', 'fakehost'),
- ('block_migration', False),
+ ('live_migration', True),
+ ('block_migration', None),
('disk_overcommit', True),
('wait', False),
]
@@ -4159,44 +4820,23 @@ class TestServerMigrate(TestServer):
result = self.cmd.take_action(parsed_args)
self.servers_mock.get.assert_called_with(self.server.id)
- self.server.live_migrate.assert_called_with(block_migration=False,
- disk_over_commit=True,
- host='fakehost')
+ self.server.live_migrate.assert_called_with(
+ block_migration=False,
+ disk_over_commit=True,
+ host=None)
self.assertNotCalled(self.servers_mock.migrate)
self.assertIsNone(result)
- def test_server_live_migrate_with_false_value_options(self):
+ def test_server_live_migrate_with_disk_overcommit_post_v224(self):
arglist = [
- '--live', 'fakehost', '--no-disk-overcommit',
- '--shared-migration', self.server.id,
- ]
- verifylist = [
- ('live', 'fakehost'),
- ('block_migration', False),
- ('disk_overcommit', False),
- ('wait', False),
- ]
- parsed_args = self.check_parser(self.cmd, arglist, verifylist)
-
- self.app.client_manager.compute.api_version = \
- api_versions.APIVersion('2.24')
-
- result = self.cmd.take_action(parsed_args)
-
- self.servers_mock.get.assert_called_with(self.server.id)
- self.server.live_migrate.assert_called_with(block_migration=False,
- disk_over_commit=False,
- host='fakehost')
- self.assertNotCalled(self.servers_mock.migrate)
- self.assertIsNone(result)
-
- def test_server_live_migrate_225(self):
- arglist = [
- '--live', 'fakehost', self.server.id,
+ '--live-migration',
+ '--disk-overcommit',
+ self.server.id,
]
verifylist = [
- ('live', 'fakehost'),
- ('block_migration', False),
+ ('live_migration', True),
+ ('block_migration', None),
+ ('disk_overcommit', True),
('wait', False),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
@@ -4204,13 +4844,21 @@ class TestServerMigrate(TestServer):
self.app.client_manager.compute.api_version = \
api_versions.APIVersion('2.25')
- result = self.cmd.take_action(parsed_args)
+ with mock.patch.object(self.cmd.log, 'warning') as mock_warning:
+ result = self.cmd.take_action(parsed_args)
self.servers_mock.get.assert_called_with(self.server.id)
- self.server.live_migrate.assert_called_with(block_migration=False,
- host='fakehost')
+ # There should be no 'disk_over_commit' value present
+ self.server.live_migrate.assert_called_with(
+ block_migration='auto',
+ host=None)
self.assertNotCalled(self.servers_mock.migrate)
self.assertIsNone(result)
+ # A warning should have been logged for using --disk-overcommit.
+ mock_warning.assert_called_once()
+ self.assertIn(
+ 'The --disk-overcommit and --no-disk-overcommit options ',
+ str(mock_warning.call_args[0][0]))
@mock.patch.object(common_utils, 'wait_for_status', return_value=True)
def test_server_migrate_with_wait(self, mock_wait_for_status):
@@ -4218,8 +4866,8 @@ class TestServerMigrate(TestServer):
'--wait', self.server.id,
]
verifylist = [
- ('live', None),
- ('block_migration', False),
+ ('live_migration', False),
+ ('block_migration', None),
('disk_overcommit', False),
('wait', True),
]
@@ -4238,8 +4886,8 @@ class TestServerMigrate(TestServer):
'--wait', self.server.id,
]
verifylist = [
- ('live', None),
- ('block_migration', False),
+ ('live_migration', False),
+ ('block_migration', None),
('disk_overcommit', False),
('wait', True),
]
@@ -4267,8 +4915,8 @@ class TestListMigration(TestServer):
self.server = compute_fakes.FakeServer.create_one_server()
self.servers_mock.get.return_value = self.server
- self.migrations = compute_fakes.FakeServerMigration\
- .create_server_migrations(count=3)
+ self.migrations = compute_fakes.FakeMigration.create_migrations(
+ count=3)
self.migrations_mock.list.return_value = self.migrations
self.data = (common_utils.get_item_properties(
@@ -4759,6 +5407,124 @@ class TestListMigrationV280(TestListMigration):
str(ex))
+class TestServerMigrationShow(TestServer):
+
+ def setUp(self):
+ super().setUp()
+
+ self.server = compute_fakes.FakeServer.create_one_server()
+ self.servers_mock.get.return_value = self.server
+
+ self.server_migration = compute_fakes.FakeServerMigration\
+ .create_one_server_migration()
+ self.server_migrations_mock.get.return_value = self.server_migration
+
+ self.columns = (
+ 'ID',
+ 'Server UUID',
+ 'Status',
+ 'Source Compute',
+ 'Source Node',
+ 'Dest Compute',
+ 'Dest Host',
+ 'Dest Node',
+ 'Memory Total Bytes',
+ 'Memory Processed Bytes',
+ 'Memory Remaining Bytes',
+ 'Disk Total Bytes',
+ 'Disk Processed Bytes',
+ 'Disk Remaining Bytes',
+ 'Created At',
+ 'Updated At',
+ )
+
+ self.data = (
+ self.server_migration.id,
+ self.server_migration.server_uuid,
+ self.server_migration.status,
+ self.server_migration.source_compute,
+ self.server_migration.source_node,
+ self.server_migration.dest_compute,
+ self.server_migration.dest_host,
+ self.server_migration.dest_node,
+ self.server_migration.memory_total_bytes,
+ self.server_migration.memory_processed_bytes,
+ self.server_migration.memory_remaining_bytes,
+ self.server_migration.disk_total_bytes,
+ self.server_migration.disk_processed_bytes,
+ self.server_migration.disk_remaining_bytes,
+ self.server_migration.created_at,
+ self.server_migration.updated_at,
+ )
+
+ # Get the command object to test
+ self.cmd = server.ShowMigration(self.app, None)
+
+ def _test_server_migration_show(self):
+ arglist = [
+ self.server.id,
+ '2', # arbitrary migration ID
+ ]
+ verifylist = []
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ columns, data = self.cmd.take_action(parsed_args)
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(self.data, data)
+
+ self.servers_mock.get.assert_called_with(self.server.id)
+ self.server_migrations_mock.get.assert_called_with(
+ self.server.id, '2',)
+
+ def test_server_migration_show(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.24')
+
+ self._test_server_migration_show()
+
+ def test_server_migration_show_v259(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.59')
+
+ self.columns += ('UUID',)
+ self.data += (self.server_migration.uuid,)
+
+ self._test_server_migration_show()
+
+ def test_server_migration_show_v280(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.80')
+
+ self.columns += ('UUID', 'User ID', 'Project ID')
+ self.data += (
+ self.server_migration.uuid,
+ self.server_migration.user_id,
+ self.server_migration.project_id,
+ )
+
+ self._test_server_migration_show()
+
+ def test_server_migration_show_pre_v224(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.23')
+
+ arglist = [
+ self.server.id,
+ '2', # arbitrary migration ID
+ ]
+ verifylist = []
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action,
+ parsed_args)
+ self.assertIn(
+ '--os-compute-api-version 2.24 or greater is required',
+ str(ex))
+
+
class TestServerMigrationAbort(TestServer):
def setUp(self):
@@ -6154,6 +6920,82 @@ class TestServerResizeConfirm(TestServer):
self.server.confirm_resize.assert_called_with()
+# TODO(stephenfin): Remove in OSC 7.0
+class TestServerMigrateConfirm(TestServer):
+
+ def setUp(self):
+ super().setUp()
+
+ methods = {
+ 'confirm_resize': None,
+ }
+ self.server = compute_fakes.FakeServer.create_one_server(
+ methods=methods)
+
+ # This is the return value for utils.find_resource()
+ self.servers_mock.get.return_value = self.server
+
+ self.servers_mock.confirm_resize.return_value = None
+
+ # Get the command object to test
+ self.cmd = server.MigrateConfirm(self.app, None)
+
+ def test_migrate_confirm(self):
+ arglist = [
+ self.server.id,
+ ]
+ verifylist = [
+ ('server', self.server.id),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ with mock.patch.object(self.cmd.log, 'warning') as mock_warning:
+ self.cmd.take_action(parsed_args)
+
+ self.servers_mock.get.assert_called_with(self.server.id)
+ self.server.confirm_resize.assert_called_with()
+
+ mock_warning.assert_called_once()
+ self.assertIn(
+ "The 'server migrate confirm' command has been deprecated",
+ str(mock_warning.call_args[0][0])
+ )
+
+
+class TestServerConfirmMigration(TestServerResizeConfirm):
+
+ def setUp(self):
+ super().setUp()
+
+ methods = {
+ 'confirm_resize': None,
+ }
+ self.server = compute_fakes.FakeServer.create_one_server(
+ methods=methods)
+
+ # This is the return value for utils.find_resource()
+ self.servers_mock.get.return_value = self.server
+
+ self.servers_mock.confirm_resize.return_value = None
+
+ # Get the command object to test
+ self.cmd = server.ConfirmMigration(self.app, None)
+
+ def test_migration_confirm(self):
+ arglist = [
+ self.server.id,
+ ]
+ verifylist = [
+ ('server', self.server.id),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ self.cmd.take_action(parsed_args)
+
+ self.servers_mock.get.assert_called_with(self.server.id)
+ self.server.confirm_resize.assert_called_with()
+
+
class TestServerResizeRevert(TestServer):
def setUp(self):
@@ -6188,6 +7030,82 @@ class TestServerResizeRevert(TestServer):
self.server.revert_resize.assert_called_with()
+# TODO(stephenfin): Remove in OSC 7.0
+class TestServerMigrateRevert(TestServer):
+
+ def setUp(self):
+ super().setUp()
+
+ methods = {
+ 'revert_resize': None,
+ }
+ self.server = compute_fakes.FakeServer.create_one_server(
+ methods=methods)
+
+ # This is the return value for utils.find_resource()
+ self.servers_mock.get.return_value = self.server
+
+ self.servers_mock.revert_resize.return_value = None
+
+ # Get the command object to test
+ self.cmd = server.MigrateRevert(self.app, None)
+
+ def test_migrate_revert(self):
+ arglist = [
+ self.server.id,
+ ]
+ verifylist = [
+ ('server', self.server.id),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ with mock.patch.object(self.cmd.log, 'warning') as mock_warning:
+ self.cmd.take_action(parsed_args)
+
+ self.servers_mock.get.assert_called_with(self.server.id)
+ self.server.revert_resize.assert_called_with()
+
+ mock_warning.assert_called_once()
+ self.assertIn(
+ "The 'server migrate revert' command has been deprecated",
+ str(mock_warning.call_args[0][0])
+ )
+
+
+class TestServerRevertMigration(TestServer):
+
+ def setUp(self):
+ super().setUp()
+
+ methods = {
+ 'revert_resize': None,
+ }
+ self.server = compute_fakes.FakeServer.create_one_server(
+ methods=methods)
+
+ # This is the return value for utils.find_resource()
+ self.servers_mock.get.return_value = self.server
+
+ self.servers_mock.revert_resize.return_value = None
+
+ # Get the command object to test
+ self.cmd = server.RevertMigration(self.app, None)
+
+ def test_migration_revert(self):
+ arglist = [
+ self.server.id,
+ ]
+ verifylist = [
+ ('server', self.server.id),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ self.cmd.take_action(parsed_args)
+
+ self.servers_mock.get.assert_called_with(self.server.id)
+ self.server.revert_resize.assert_called_with()
+
+
class TestServerRestore(TestServer):
def setUp(self):
@@ -6781,6 +7699,28 @@ class TestServerStart(TestServer):
def test_server_start_multi_servers(self):
self.run_method_with_servers('start', 3)
+ @mock.patch.object(common_utils, 'find_resource')
+ def test_server_start_with_all_projects(self, mock_find_resource):
+ servers = self.setup_servers_mock(count=1)
+ mock_find_resource.side_effect = compute_fakes.FakeServer.get_servers(
+ servers, 0,
+ )
+
+ arglist = [
+ servers[0].id,
+ '--all-projects',
+ ]
+ verifylist = [
+ ('server', [servers[0].id]),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ self.cmd.take_action(parsed_args)
+
+ mock_find_resource.assert_called_once_with(
+ mock.ANY, servers[0].id, all_tenants=True,
+ )
+
class TestServerStop(TestServer):
@@ -6801,6 +7741,28 @@ class TestServerStop(TestServer):
def test_server_stop_multi_servers(self):
self.run_method_with_servers('stop', 3)
+ @mock.patch.object(common_utils, 'find_resource')
+ def test_server_start_with_all_projects(self, mock_find_resource):
+ servers = self.setup_servers_mock(count=1)
+ mock_find_resource.side_effect = compute_fakes.FakeServer.get_servers(
+ servers, 0,
+ )
+
+ arglist = [
+ servers[0].id,
+ '--all-projects',
+ ]
+ verifylist = [
+ ('server', [servers[0].id]),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ self.cmd.take_action(parsed_args)
+
+ mock_find_resource.assert_called_once_with(
+ mock.ANY, servers[0].id, all_tenants=True,
+ )
+
class TestServerSuspend(TestServer):
diff --git a/openstackclient/tests/unit/compute/v2/test_server_event.py b/openstackclient/tests/unit/compute/v2/test_server_event.py
index 5c94891a..058a44d8 100644
--- a/openstackclient/tests/unit/compute/v2/test_server_event.py
+++ b/openstackclient/tests/unit/compute/v2/test_server_event.py
@@ -11,7 +11,12 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
-#
+
+from unittest import mock
+
+import iso8601
+from novaclient import api_versions
+from osc_lib import exceptions
from openstackclient.compute.v2 import server_event
from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes
@@ -113,6 +118,234 @@ class TestListServerEvent(TestServerEvent):
self.assertEqual(self.long_columns, columns)
self.assertEqual(self.long_data, tuple(data))
+ def test_server_event_list_with_changes_since(self):
+ self.app.client_manager.compute.api_version = \
+ api_versions.APIVersion('2.58')
+
+ arglist = [
+ '--changes-since', '2016-03-04T06:27:59Z',
+ self.fake_server.name,
+ ]
+ verifylist = [
+ ('server', self.fake_server.name),
+ ('changes_since', '2016-03-04T06:27:59Z'),
+ ]
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ columns, data = self.cmd.take_action(parsed_args)
+
+ self.servers_mock.get.assert_called_once_with(self.fake_server.name)
+ self.events_mock.list.assert_called_once_with(
+ self.fake_server.id, changes_since='2016-03-04T06:27:59Z')
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(tuple(self.data), tuple(data))
+
+ @mock.patch.object(iso8601, 'parse_date', side_effect=iso8601.ParseError)
+ def test_server_event_list_with_changes_since_invalid(
+ self, mock_parse_isotime,
+ ):
+ self.app.client_manager.compute.api_version = \
+ api_versions.APIVersion('2.58')
+
+ arglist = [
+ '--changes-since', 'Invalid time value',
+ self.fake_server.name,
+ ]
+ verifylist = [
+ ('server', self.fake_server.name),
+ ('changes_since', 'Invalid time value'),
+ ]
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action,
+ parsed_args)
+
+ self.assertIn(
+ 'Invalid changes-since value:', str(ex))
+ mock_parse_isotime.assert_called_once_with(
+ 'Invalid time value'
+ )
+
+ def test_server_event_list_with_changes_since_pre_v258(self):
+ self.app.client_manager.compute.api_version = \
+ api_versions.APIVersion('2.57')
+
+ arglist = [
+ '--changes-since', '2016-03-04T06:27:59Z',
+ self.fake_server.name,
+ ]
+ verifylist = [
+ ('server', self.fake_server.name),
+ ('changes_since', '2016-03-04T06:27:59Z'),
+ ]
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action,
+ parsed_args)
+
+ self.assertIn(
+ '--os-compute-api-version 2.58 or greater is required', str(ex))
+
+ def test_server_event_list_with_changes_before(self):
+ self.app.client_manager.compute.api_version = \
+ api_versions.APIVersion('2.66')
+
+ arglist = [
+ '--changes-before', '2016-03-04T06:27:59Z',
+ self.fake_server.name,
+ ]
+ verifylist = [
+ ('server', self.fake_server.name),
+ ('changes_before', '2016-03-04T06:27:59Z'),
+ ]
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ columns, data = self.cmd.take_action(parsed_args)
+
+ self.servers_mock.get.assert_called_once_with(self.fake_server.name)
+ self.events_mock.list.assert_called_once_with(
+ self.fake_server.id, changes_before='2016-03-04T06:27:59Z')
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(tuple(self.data), tuple(data))
+
+ @mock.patch.object(iso8601, 'parse_date', side_effect=iso8601.ParseError)
+ def test_server_event_list_with_changes_before_invalid(
+ self, mock_parse_isotime,
+ ):
+ self.app.client_manager.compute.api_version = \
+ api_versions.APIVersion('2.66')
+
+ arglist = [
+ '--changes-before', 'Invalid time value',
+ self.fake_server.name,
+ ]
+ verifylist = [
+ ('server', self.fake_server.name),
+ ('changes_before', 'Invalid time value'),
+ ]
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action,
+ parsed_args)
+
+ self.assertIn(
+ 'Invalid changes-before value:', str(ex))
+ mock_parse_isotime.assert_called_once_with(
+ 'Invalid time value'
+ )
+
+ def test_server_event_list_with_changes_before_pre_v266(self):
+ self.app.client_manager.compute.api_version = \
+ api_versions.APIVersion('2.65')
+
+ arglist = [
+ '--changes-before', '2016-03-04T06:27:59Z',
+ self.fake_server.name,
+ ]
+ verifylist = [
+ ('server', self.fake_server.name),
+ ('changes_before', '2016-03-04T06:27:59Z'),
+ ]
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action,
+ parsed_args)
+
+ self.assertIn(
+ '--os-compute-api-version 2.66 or greater is required', str(ex))
+
+ def test_server_event_list_with_limit(self):
+ self.app.client_manager.compute.api_version = \
+ api_versions.APIVersion('2.58')
+
+ arglist = [
+ '--limit', '1',
+ self.fake_server.name,
+ ]
+ verifylist = [
+ ('limit', 1),
+ ('server', self.fake_server.name),
+ ]
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ self.cmd.take_action(parsed_args)
+
+ self.events_mock.list.assert_called_once_with(
+ self.fake_server.id, limit=1)
+
+ def test_server_event_list_with_limit_pre_v258(self):
+ self.app.client_manager.compute.api_version = \
+ api_versions.APIVersion('2.57')
+
+ arglist = [
+ '--limit', '1',
+ self.fake_server.name,
+ ]
+ verifylist = [
+ ('limit', 1),
+ ('server', self.fake_server.name),
+ ]
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action,
+ parsed_args)
+
+ self.assertIn(
+ '--os-compute-api-version 2.58 or greater is required', str(ex))
+
+ def test_server_event_list_with_marker(self):
+ self.app.client_manager.compute.api_version = \
+ api_versions.APIVersion('2.58')
+
+ arglist = [
+ '--marker', 'test_event',
+ self.fake_server.name,
+ ]
+ verifylist = [
+ ('marker', 'test_event'),
+ ('server', self.fake_server.name),
+ ]
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ self.cmd.take_action(parsed_args)
+
+ self.events_mock.list.assert_called_once_with(
+ self.fake_server.id, marker='test_event')
+
+ def test_server_event_list_with_marker_pre_v258(self):
+ self.app.client_manager.compute.api_version = \
+ api_versions.APIVersion('2.57')
+
+ arglist = [
+ '--marker', 'test_event',
+ self.fake_server.name,
+ ]
+ verifylist = [
+ ('marker', 'test_event'),
+ ('server', self.fake_server.name),
+ ]
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action,
+ parsed_args)
+
+ self.assertIn(
+ '--os-compute-api-version 2.58 or greater is required', str(ex))
+
class TestShowServerEvent(TestServerEvent):
diff --git a/openstackclient/tests/unit/compute/v2/test_server_group.py b/openstackclient/tests/unit/compute/v2/test_server_group.py
index 732c1881..3ed19e27 100644
--- a/openstackclient/tests/unit/compute/v2/test_server_group.py
+++ b/openstackclient/tests/unit/compute/v2/test_server_group.py
@@ -326,10 +326,13 @@ class TestServerGroupList(TestServerGroup):
verifylist = [
('all_projects', False),
('long', False),
+ ('limit', None),
+ ('offset', None),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
- self.server_groups_mock.list.assert_called_once_with(False)
+
+ self.server_groups_mock.list.assert_called_once_with()
self.assertCountEqual(self.list_columns, columns)
self.assertCountEqual(self.list_data, tuple(data))
@@ -342,14 +345,49 @@ class TestServerGroupList(TestServerGroup):
verifylist = [
('all_projects', True),
('long', True),
+ ('limit', None),
+ ('offset', None),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
- self.server_groups_mock.list.assert_called_once_with(True)
+ self.server_groups_mock.list.assert_called_once_with(
+ all_projects=True)
self.assertCountEqual(self.list_columns_long, columns)
self.assertCountEqual(self.list_data_long, tuple(data))
+ def test_server_group_list_with_limit(self):
+ arglist = [
+ '--limit', '1',
+ ]
+ verifylist = [
+ ('all_projects', False),
+ ('long', False),
+ ('limit', 1),
+ ('offset', None),
+ ]
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ self.cmd.take_action(parsed_args)
+
+ self.server_groups_mock.list.assert_called_once_with(limit=1)
+
+ def test_server_group_list_with_offset(self):
+ arglist = [
+ '--offset', '5',
+ ]
+ verifylist = [
+ ('all_projects', False),
+ ('long', False),
+ ('limit', None),
+ ('offset', 5),
+ ]
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ self.cmd.take_action(parsed_args)
+
+ self.server_groups_mock.list.assert_called_once_with(offset=5)
+
class TestServerGroupListV264(TestServerGroupV264):
@@ -400,7 +438,7 @@ class TestServerGroupListV264(TestServerGroupV264):
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
- self.server_groups_mock.list.assert_called_once_with(False)
+ self.server_groups_mock.list.assert_called_once_with()
self.assertCountEqual(self.list_columns, columns)
self.assertCountEqual(self.list_data, tuple(data))
@@ -416,7 +454,8 @@ class TestServerGroupListV264(TestServerGroupV264):
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
- self.server_groups_mock.list.assert_called_once_with(True)
+ self.server_groups_mock.list.assert_called_once_with(
+ all_projects=True)
self.assertCountEqual(self.list_columns_long, columns)
self.assertCountEqual(self.list_data_long, tuple(data))
diff --git a/openstackclient/tests/unit/network/v2/test_network_rbac.py b/openstackclient/tests/unit/network/v2/test_network_rbac.py
index d7c71ea7..08be64c5 100644
--- a/openstackclient/tests/unit/network/v2/test_network_rbac.py
+++ b/openstackclient/tests/unit/network/v2/test_network_rbac.py
@@ -42,6 +42,7 @@ class TestCreateNetworkRBAC(TestNetworkRBAC):
sg_object = network_fakes.FakeNetworkSecGroup.create_one_security_group()
as_object = network_fakes.FakeAddressScope.create_one_address_scope()
snp_object = network_fakes.FakeSubnetPool.create_one_subnet_pool()
+ ag_object = network_fakes.FakeAddressGroup.create_one_address_group()
project = identity_fakes_v3.FakeProject.create_one_project()
rbac_policy = network_fakes.FakeNetworkRBAC.create_one_network_rbac(
attrs={'tenant_id': project.id,
@@ -85,6 +86,8 @@ class TestCreateNetworkRBAC(TestNetworkRBAC):
return_value=self.as_object)
self.network.find_subnet_pool = mock.Mock(
return_value=self.snp_object)
+ self.network.find_address_group = mock.Mock(
+ return_value=self.ag_object)
self.projects_mock.get.return_value = self.project
def test_network_rbac_create_no_type(self):
@@ -236,7 +239,8 @@ class TestCreateNetworkRBAC(TestNetworkRBAC):
('qos_policy', "qos_object"),
('security_group', "sg_object"),
('subnetpool', "snp_object"),
- ('address_scope', "as_object")
+ ('address_scope', "as_object"),
+ ('address_group', "ag_object")
)
@ddt.unpack
def test_network_rbac_create_object(self, obj_type, obj_fake_attr):
diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py
index c8bced71..8c5158d7 100644
--- a/openstackclient/tests/unit/network/v2/test_port.py
+++ b/openstackclient/tests/unit/network/v2/test_port.py
@@ -1250,6 +1250,26 @@ class TestListPort(TestPort):
self.assertEqual(self.columns, columns)
self.assertItemsEqual(self.data, list(data))
+ def test_port_list_name(self):
+ test_name = "fakename"
+ arglist = [
+ '--name', test_name,
+ ]
+ verifylist = [
+ ('name', test_name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ columns, data = self.cmd.take_action(parsed_args)
+ filters = {
+ 'name': test_name,
+ 'fields': LIST_FIELDS_TO_RETRIEVE,
+ }
+
+ self.network.ports.assert_called_once_with(**filters)
+ self.assertEqual(self.columns, columns)
+ self.assertItemsEqual(self.data, list(data))
+
def test_list_with_tag_options(self):
arglist = [
'--tags', 'red,blue',
diff --git a/openstackclient/tests/unit/network/v2/test_subnet.py b/openstackclient/tests/unit/network/v2/test_subnet.py
index 1b4bfdad..6085cda8 100644
--- a/openstackclient/tests/unit/network/v2/test_subnet.py
+++ b/openstackclient/tests/unit/network/v2/test_subnet.py
@@ -1264,6 +1264,7 @@ class TestUnsetSubnet(TestSubnet):
'end': '8.8.8.170'}],
'service_types': ['network:router_gateway',
'network:floatingip_agent_gateway'],
+ 'gateway_ip': 'fe80::a00a:0:c0de:0:1',
'tags': ['green', 'red'], })
self.network.find_subnet = mock.Mock(return_value=self._testsubnet)
self.network.update_subnet = mock.Mock(return_value=None)
@@ -1277,6 +1278,7 @@ class TestUnsetSubnet(TestSubnet):
'--host-route', 'destination=10.30.30.30/24,gateway=10.30.30.1',
'--allocation-pool', 'start=8.8.8.100,end=8.8.8.150',
'--service-type', 'network:router_gateway',
+ '--gateway',
self._testsubnet.name,
]
verifylist = [
@@ -1286,6 +1288,7 @@ class TestUnsetSubnet(TestSubnet):
('allocation_pools', [{
'start': '8.8.8.100', 'end': '8.8.8.150'}]),
('service_types', ['network:router_gateway']),
+ ('gateway', True),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
@@ -1297,6 +1300,7 @@ class TestUnsetSubnet(TestSubnet):
"destination": "10.20.20.0/24", "nexthop": "10.20.20.1"}],
'allocation_pools': [{'start': '8.8.8.160', 'end': '8.8.8.170'}],
'service_types': ['network:floatingip_agent_gateway'],
+ 'gateway_ip': None,
}
self.network.update_subnet.assert_called_once_with(
self._testsubnet, **attrs)