diff options
Diffstat (limited to 'openstackclient')
| -rw-r--r-- | openstackclient/compute/v2/server.py | 1124 | ||||
| -rw-r--r-- | openstackclient/compute/v2/server_event.py | 178 | ||||
| -rw-r--r-- | openstackclient/compute/v2/server_group.py | 38 | ||||
| -rw-r--r-- | openstackclient/network/v2/network_rbac.py | 22 | ||||
| -rw-r--r-- | openstackclient/network/v2/port.py | 7 | ||||
| -rw-r--r-- | openstackclient/network/v2/subnet.py | 7 | ||||
| -rw-r--r-- | openstackclient/tests/functional/compute/v2/test_server.py | 164 | ||||
| -rw-r--r-- | openstackclient/tests/unit/compute/v2/fakes.py | 73 | ||||
| -rw-r--r-- | openstackclient/tests/unit/compute/v2/test_server.py | 1440 | ||||
| -rw-r--r-- | openstackclient/tests/unit/compute/v2/test_server_event.py | 235 | ||||
| -rw-r--r-- | openstackclient/tests/unit/compute/v2/test_server_group.py | 47 | ||||
| -rw-r--r-- | openstackclient/tests/unit/network/v2/test_network_rbac.py | 6 | ||||
| -rw-r--r-- | openstackclient/tests/unit/network/v2/test_port.py | 20 | ||||
| -rw-r--r-- | openstackclient/tests/unit/network/v2/test_subnet.py | 4 |
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) |
