diff options
Diffstat (limited to 'openstackclient')
17 files changed, 1445 insertions, 324 deletions
diff --git a/openstackclient/compute/v2/console.py b/openstackclient/compute/v2/console.py index 110b21b8..0ab5c8a2 100644 --- a/openstackclient/compute/v2/console.py +++ b/openstackclient/compute/v2/console.py @@ -22,6 +22,15 @@ from osc_lib import utils from openstackclient.i18n import _ +def _get_console_columns(item): + # To maintain backwards compatibility we need to rename sdk props to + # whatever OSC was using before + column_map = {} + hidden_columns = ['id', 'links', 'location', 'name'] + return utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden_columns) + + class ShowConsoleLog(command.Command): _description = _("Show server's console output") @@ -44,19 +53,18 @@ class ShowConsoleLog(command.Command): return parser def take_action(self, parsed_args): - compute_client = self.app.client_manager.compute + compute_client = self.app.client_manager.sdk_connection.compute - server = utils.find_resource( - compute_client.servers, - parsed_args.server, + server = compute_client.find_server( + name_or_id=parsed_args.server, + ignore_missing=False ) - length = parsed_args.lines - if length: - # NOTE(dtroyer): get_console_output() appears to shortchange the - # output by one line - length += 1 - data = server.get_console_output(length=length) + output = compute_client.get_server_console_output( + server.id, length=parsed_args.lines) + data = None + if output: + data = output.get('output', None) if data and data[-1] != '\n': data += '\n' @@ -120,21 +128,15 @@ class ShowConsoleURL(command.ShowOne): return parser def take_action(self, parsed_args): - compute_client = self.app.client_manager.compute - server = utils.find_resource( - compute_client.servers, + compute_client = self.app.client_manager.sdk_connection.compute + server = compute_client.find_server( parsed_args.server, - ) + ignore_missing=False) + + data = compute_client.create_console(server.id, + console_type=parsed_args.url_type) + + display_columns, columns = _get_console_columns(data) + data = utils.get_dict_properties(data, columns) - data = server.get_console_url(parsed_args.url_type) - if not data: - return ({}, {}) - - info = {} - # NOTE(Rui Chen): Return 'remote_console' in compute microversion API - # 2.6 and later, return 'console' in compute - # microversion API from 2.0 to 2.5, do compatibility - # handle for different microversion API. - console_data = data.get('remote_console', data.get('console')) - info.update(console_data) - return zip(*sorted(info.items())) + return (display_columns, data) diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index 805e919e..00431b7b 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -18,6 +18,7 @@ import logging from novaclient import api_versions +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -30,6 +31,31 @@ from openstackclient.identity import common as identity_common LOG = logging.getLogger(__name__) +_formatters = { + 'extra_specs': format_columns.DictColumn, + # Unless we finish switch to use SDK resources this need to be doubled this + # way + 'properties': format_columns.DictColumn, + 'Properties': format_columns.DictColumn +} + + +def _get_flavor_columns(item): + # To maintain backwards compatibility we need to rename sdk props to + # whatever OSC was using before + column_map = { + 'extra_specs': 'properties', + 'ephemeral': 'OS-FLV-EXT-DATA:ephemeral', + 'is_disabled': 'OS-FLV-DISABLED:disabled', + 'is_public': 'os-flavor-access:is_public' + + } + hidden_columns = ['links', 'location'] + + return utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden_columns) + + def _find_flavor(compute_client, flavor): try: return compute_client.flavors.get(flavor) @@ -191,10 +217,16 @@ class CreateFlavor(command.ShowOne): LOG.error(_("Failed to set flavor property: %s"), e) flavor_info = flavor._info.copy() - flavor_info.pop("links") - flavor_info['properties'] = utils.format_dict(flavor.get_keys()) + flavor_info['properties'] = flavor.get_keys() - return zip(*sorted(flavor_info.items())) + display_columns, columns = _get_flavor_columns(flavor_info) + data = utils.get_dict_properties( + flavor_info, columns, + formatters=_formatters, + mixed_case_fields=['OS-FLV-DISABLED:disabled', + 'OS-FLV-EXT-DATA:ephemeral']) + + return (display_columns, data) class DeleteFlavor(command.Command): @@ -309,7 +341,7 @@ class ListFlavor(command.Lister): return (column_headers, (utils.get_item_properties( - s, columns, formatters={'Properties': utils.format_dict}, + s, columns, formatters=_formatters, ) for s in data)) @@ -428,11 +460,8 @@ class ShowFlavor(command.ShowOne): try: flavor_access = compute_client.flavor_access.list( flavor=resource_flavor.id) - projects = [utils.get_field(access, 'tenant_id') - for access in flavor_access] - # TODO(Huanxuan Ao): This format case can be removed after - # patch https://review.opendev.org/#/c/330223/ merged. - access_projects = utils.format_list(projects) + access_projects = [utils.get_field(access, 'tenant_id') + for access in flavor_access] except Exception as e: msg = _("Failed to get access projects list " "for flavor '%(flavor)s': %(e)s") @@ -442,11 +471,17 @@ class ShowFlavor(command.ShowOne): flavor.update({ 'access_project_ids': access_projects }) - flavor.pop("links", None) - flavor['properties'] = utils.format_dict(resource_flavor.get_keys()) + flavor['properties'] = resource_flavor.get_keys() + + display_columns, columns = _get_flavor_columns(flavor) + data = utils.get_dict_properties( + flavor, columns, + formatters=_formatters, + mixed_case_fields=['OS-FLV-DISABLED:disabled', + 'OS-FLV-EXT-DATA:ephemeral']) - return zip(*sorted(flavor.items())) + return (display_columns, data) class UnsetFlavor(command.Command): diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py index 2b365ceb..8c365cf0 100644 --- a/openstackclient/compute/v2/keypair.py +++ b/openstackclient/compute/v2/keypair.py @@ -20,11 +20,13 @@ import logging import os import sys +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 _ +from openstackclient.identity import common as identity_common LOG = logging.getLogger(__name__) @@ -53,6 +55,15 @@ class CreateKeypair(command.ShowOne): help=_("Filename for private key to save. If not used, " "print private key in console.") ) + parser.add_argument( + '--type', + metavar='<type>', + choices=['ssh', 'x509'], + help=_( + "Keypair type. Can be ssh or x509. " + "(Supported by API versions '2.2' - '2.latest')" + ), + ) return parser def take_action(self, parsed_args): @@ -70,17 +81,28 @@ class CreateKeypair(command.ShowOne): "exception": e} ) - keypair = compute_client.keypairs.create( - parsed_args.name, - public_key=public_key, - ) + kwargs = { + 'name': parsed_args.name, + 'public_key': public_key, + } + if parsed_args.type: + if compute_client.api_version < api_versions.APIVersion('2.2'): + msg = _( + '--os-compute-api-version 2.2 or greater is required to ' + 'support the --type option.' + ) + raise exceptions.CommandError(msg) + + kwargs['key_type'] = parsed_args.type + + keypair = compute_client.keypairs.create(**kwargs) private_key = parsed_args.private_key # Save private key into specified file if private_key: try: with io.open( - os.path.expanduser(parsed_args.private_key), 'w+' + os.path.expanduser(parsed_args.private_key), 'w+' ) as p: p.write(keypair.private_key) except IOError as e: @@ -142,18 +164,85 @@ class DeleteKeypair(command.Command): class ListKeypair(command.Lister): _description = _("List key fingerprints") + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + user_group = parser.add_mutually_exclusive_group() + user_group.add_argument( + '--user', + metavar='<user>', + help=_( + 'Show keypairs for another user (admin only) (name or ID). ' + 'Requires ``--os-compute-api-version`` 2.10 or greater.' + ), + ) + identity_common.add_user_domain_option_to_parser(parser) + user_group.add_argument( + '--project', + metavar='<project>', + help=_( + 'Show keypairs for all users associated with project ' + '(admin only) (name or ID). ' + 'Requires ``--os-compute-api-version`` 2.10 or greater.' + ), + ) + identity_common.add_project_domain_option_to_parser(parser) + return parser + def take_action(self, parsed_args): compute_client = self.app.client_manager.compute + identity_client = self.app.client_manager.identity + + if parsed_args.project: + if compute_client.api_version < api_versions.APIVersion('2.10'): + msg = _( + '--os-compute-api-version 2.10 or greater is required to ' + 'support the --project option' + ) + raise exceptions.CommandError(msg) + + # NOTE(stephenfin): This is done client side because nova doesn't + # currently support doing so server-side. If this is slow, we can + # think about spinning up a threadpool or similar. + project = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + users = identity_client.users.list(tenant_id=project) + + data = [] + for user in users: + data.extend(compute_client.keypairs.list(user_id=user.id)) + elif parsed_args.user: + if compute_client.api_version < api_versions.APIVersion('2.10'): + msg = _( + '--os-compute-api-version 2.10 or greater is required to ' + 'support the --user option' + ) + raise exceptions.CommandError(msg) + + user = identity_common.find_user( + identity_client, + parsed_args.user, + parsed_args.user_domain, + ) + + data = compute_client.keypairs.list(user_id=user.id) + else: + data = compute_client.keypairs.list() + columns = ( "Name", "Fingerprint" ) - data = compute_client.keypairs.list() - return (columns, - (utils.get_item_properties( - s, columns, - ) for s in data)) + if compute_client.api_version >= api_versions.APIVersion('2.2'): + columns += ("Type", ) + + return ( + columns, + (utils.get_item_properties(s, columns) for s in data), + ) class ShowKeypair(command.ShowOne): diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index dec6eb62..fddafaee 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -236,6 +236,14 @@ class AddFixedIP(command.Command): metavar="<ip-address>", help=_("Requested fixed IP address"), ) + parser.add_argument( + '--tag', + metavar='<tag>', + help=_( + 'Tag for the attached interface. ' + '(supported by --os-compute-api-version 2.52 or above)' + ) + ) return parser def take_action(self, parsed_args): @@ -246,11 +254,23 @@ class AddFixedIP(command.Command): network = compute_client.api.network_find(parsed_args.network) - server.interface_attach( - port_id=None, - net_id=network['id'], - fixed_ip=parsed_args.fixed_ip_address, - ) + kwargs = { + 'port_id': None, + 'net_id': network['id'], + 'fixed_ip': parsed_args.fixed_ip_address, + } + + if parsed_args.tag: + if compute_client.api_version < api_versions.APIVersion('2.49'): + msg = _( + '--os-compute-api-version 2.49 or greater is required to ' + 'support the --tag option' + ) + raise exceptions.CommandError(msg) + + kwargs['tag'] = parsed_args.tag + + server.interface_attach(**kwargs) class AddFloatingIP(network_common.NetworkAndComputeCommand): @@ -510,20 +530,32 @@ class AddServerVolume(command.Command): metavar='<device>', help=_('Server internal device name for volume'), ) + parser.add_argument( + '--tag', + metavar='<tag>', + help=_( + "Tag for the attached volume. " + "(Supported by API versions '2.49' - '2.latest')" + ), + ) termination_group = parser.add_mutually_exclusive_group() termination_group.add_argument( '--enable-delete-on-termination', action='store_true', - help=_("Specify if the attached volume should be deleted when " - "the server is destroyed. (Supported with " - "``--os-compute-api-version`` 2.79 or greater.)"), + help=_( + "Specify if the attached volume should be deleted when the " + "server is destroyed. " + "(Supported by API versions '2.79' - '2.latest')" + ), ) termination_group.add_argument( '--disable-delete-on-termination', action='store_true', - help=_("Specify if the attached volume should not be deleted " - "when the server is destroyed. (Supported with " - "``--os-compute-api-version`` 2.79 or greater.)"), + help=_( + "Specify if the attached volume should not be deleted when " + "the server is destroyed. " + "(Supported by API versions '2.79' - '2.latest')" + ), ) return parser @@ -540,28 +572,38 @@ class AddServerVolume(command.Command): parsed_args.volume, ) - support_set_delete_on_termination = (compute_client.api_version >= - api_versions.APIVersion('2.79')) - - if not support_set_delete_on_termination: - if parsed_args.enable_delete_on_termination: - msg = _('--os-compute-api-version 2.79 or greater ' - 'is required to support the ' - '--enable-delete-on-termination option.') - raise exceptions.CommandError(msg) - if parsed_args.disable_delete_on_termination: - msg = _('--os-compute-api-version 2.79 or greater ' - 'is required to support the ' - '--disable-delete-on-termination option.') - raise exceptions.CommandError(msg) - kwargs = { "device": parsed_args.device } + if parsed_args.tag: + if compute_client.api_version < api_versions.APIVersion('2.49'): + msg = _( + '--os-compute-api-version 2.49 or greater is required to ' + 'support the --tag option' + ) + raise exceptions.CommandError(msg) + + kwargs['tag'] = parsed_args.tag + if parsed_args.enable_delete_on_termination: + if compute_client.api_version < api_versions.APIVersion('2.79'): + msg = _( + '--os-compute-api-version 2.79 or greater is required to ' + 'support the --enable-delete-on-termination option.' + ) + raise exceptions.CommandError(msg) + kwargs['delete_on_termination'] = True + if parsed_args.disable_delete_on_termination: + if compute_client.api_version < api_versions.APIVersion('2.79'): + msg = _( + '--os-compute-api-version 2.79 or greater is required to ' + 'support the --disable-delete-on-termination option.' + ) + raise exceptions.CommandError(msg) + kwargs['delete_on_termination'] = False compute_client.volumes.create_server_volume( @@ -598,13 +640,20 @@ class CreateServer(command.ShowOne): disk_group.add_argument( '--volume', metavar='<volume>', - help=_('Create server using this volume 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.'), + help=_( + 'Create server using this volume 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( + '--password', + metavar='<password>', + help=_("Set the password to this server"), ) parser.add_argument( '--flavor', @@ -617,28 +666,34 @@ class CreateServer(command.ShowOne): metavar='<security-group>', action='append', default=[], - help=_('Security group to assign to this server (name or ID) ' - '(repeat option to set multiple groups)'), + help=_( + 'Security group to assign to this server (name or ID) ' + '(repeat option to set multiple groups)' + ), ) parser.add_argument( '--key-name', metavar='<key-name>', - help=_('Keypair to inject into this server (optional extension)'), + help=_('Keypair to inject into this server'), ) parser.add_argument( '--property', metavar='<key=value>', action=parseractions.KeyValueAction, - help=_('Set a property on this server ' - '(repeat option to set multiple values)'), + help=_( + 'Set a property on this server ' + '(repeat option to set multiple values)' + ), ) parser.add_argument( '--file', metavar='<dest-filename=source-filename>', action='append', default=[], - help=_('File to inject into image before boot ' - '(repeat option to set multiple files)'), + help=_( + 'File to inject into image before boot ' + '(repeat option to set multiple files)' + ), ) parser.add_argument( '--user-data', @@ -648,8 +703,10 @@ class CreateServer(command.ShowOne): parser.add_argument( '--description', metavar='<description>', - help=_('Set description for the server (supported by ' - '--os-compute-api-version 2.19 or above)'), + help=_( + 'Set description for the server ' + '(supported by --os-compute-api-version 2.19 or above)' + ), ) parser.add_argument( '--availability-zone', @@ -659,29 +716,35 @@ class CreateServer(command.ShowOne): parser.add_argument( '--host', metavar='<host>', - help=_('Requested host to create servers. Admin only ' - 'by default. (supported by --os-compute-api-version 2.74 ' - 'or above)'), + help=_( + 'Requested host to create servers. ' + '(admin only) ' + '(supported by --os-compute-api-version 2.74 or above)' + ), ) parser.add_argument( '--hypervisor-hostname', metavar='<hypervisor-hostname>', - help=_('Requested hypervisor hostname to create servers. Admin ' - 'only by default. (supported by --os-compute-api-version ' - '2.74 or above)'), + help=_( + 'Requested hypervisor hostname to create servers. ' + '(admin only) ' + '(supported by --os-compute-api-version 2.74 or above)' + ), ) 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.') + 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', @@ -691,37 +754,40 @@ class CreateServer(command.ShowOne): # 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' - '(optional extension)'), + 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."), + 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', @@ -729,13 +795,15 @@ class CreateServer(command.ShowOne): 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."), + 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', @@ -743,12 +811,14 @@ class CreateServer(command.ShowOne): 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."), + 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', @@ -801,6 +871,18 @@ class CreateServer(command.ShowOne): action='store_true', help=_('Wait for build to complete'), ) + parser.add_argument( + '--tag', + metavar='<tag>', + action='append', + default=[], + dest='tags', + help=_( + 'Tags for the server. ' + 'Specify multiple times to add multiple tags. ' + '(supported by --os-compute-api-version 2.52 or above)' + ), + ) return parser def take_action(self, parsed_args): @@ -823,10 +905,13 @@ class CreateServer(command.ShowOne): if not image and parsed_args.image_property: def emit_duplicated_warning(img, image_property): img_uuid_list = [str(image.id) for image in img] - LOG.warning(_('Multiple matching images: %(img_uuid_list)s\n' - 'Using image: %(chosen_one)s') % - {'img_uuid_list': img_uuid_list, - 'chosen_one': img_uuid_list[0]}) + LOG.warning( + 'Multiple matching images: %(img_uuid_list)s\n' + 'Using image: %(chosen_one)s', + { + 'img_uuid_list': img_uuid_list, + 'chosen_one': img_uuid_list[0], + }) def _match_image(image_api, wanted_properties): image_list = image_api.images() @@ -843,45 +928,52 @@ class CreateServer(command.ShowOne): set([key, value]) except TypeError: if key != 'properties': - LOG.debug('Skipped the \'%s\' attribute. ' - 'That cannot be compared. ' - '(image: %s, value: %s)', - key, img.id, value) + LOG.debug( + 'Skipped the \'%s\' attribute. ' + 'That cannot be compared. ' + '(image: %s, value: %s)', + key, img.id, value, + ) pass else: img_dict[key] = value - if all(k in img_dict and img_dict[k] == v - for k, v in wanted_properties.items()): + if all( + k in img_dict and img_dict[k] == v + for k, v in wanted_properties.items() + ): images_matched.append(img) + return images_matched images = _match_image(image_client, parsed_args.image_property) if len(images) > 1: - emit_duplicated_warning(images, - parsed_args.image_property) + emit_duplicated_warning(images, parsed_args.image_property) if images: image = images[0] else: - raise exceptions.CommandError(_("No images match the " - "property expected by " - "--image-property")) + msg = _( + 'No images match the property expected by ' + '--image-property' + ) + raise exceptions.CommandError(msg) # Lookup parsed_args.volume volume = None if parsed_args.volume: # --volume and --boot-from-volume are mutually exclusive. if parsed_args.boot_from_volume: - raise exceptions.CommandError( - _('--volume is not allowed with --boot-from-volume')) + msg = _('--volume is not allowed with --boot-from-volume') + raise exceptions.CommandError(msg) + volume = utils.find_resource( volume_client.volumes, parsed_args.volume, ).id # Lookup parsed_args.flavor - flavor = utils.find_resource(compute_client.flavors, - parsed_args.flavor) + flavor = utils.find_resource( + compute_client.flavors, parsed_args.flavor) files = {} for f in parsed_args.file: @@ -891,16 +983,17 @@ class CreateServer(command.ShowOne): except IOError as e: msg = _("Can't open '%(source)s': %(exception)s") raise exceptions.CommandError( - msg % {"source": src, - "exception": e} + msg % {'source': src, 'exception': e} ) if parsed_args.min > parsed_args.max: msg = _("min instances should be <= max instances") raise exceptions.CommandError(msg) + if parsed_args.min < 1: msg = _("min instances should be > 0") raise exceptions.CommandError(msg) + if parsed_args.max < 1: msg = _("max instances should be > 0") raise exceptions.CommandError(msg) @@ -912,8 +1005,7 @@ class CreateServer(command.ShowOne): except IOError as e: msg = _("Can't open '%(data)s': %(exception)s") raise exceptions.CommandError( - msg % {"data": parsed_args.user_data, - "exception": e} + msg % {'data': parsed_args.user_data, 'exception': e} ) if parsed_args.description: @@ -924,11 +1016,12 @@ class CreateServer(command.ShowOne): block_device_mapping_v2 = [] if volume: - block_device_mapping_v2 = [{'uuid': volume, - 'boot_index': '0', - 'source_type': 'volume', - 'destination_type': 'volume' - }] + block_device_mapping_v2 = [{ + 'uuid': volume, + 'boot_index': '0', + 'source_type': 'volume', + 'destination_type': 'volume' + }] elif parsed_args.boot_from_volume: # Tell nova to create a root volume from the image provided. block_device_mapping_v2 = [{ @@ -949,13 +1042,16 @@ class CreateServer(command.ShowOne): 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': @@ -981,14 +1077,18 @@ class CreateServer(command.ShowOne): 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: - msg = _("Volume, volume snapshot or image (name or ID) must " - "be specified if --block-device-mapping is specified") + msg = _( + 'Volume, volume snapshot or image (name or ID) must ' + 'be specified if --block-device-mapping is specified' + ) raise exceptions.CommandError(msg) block_device_mapping_v2.append(mapping) @@ -1002,22 +1102,32 @@ class CreateServer(command.ShowOne): auto_or_none = True nics.append(nic_str) else: - nic_info = {"net-id": "", "v4-fixed-ip": "", - "v6-fixed-ip": "", "port-id": ""} + 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>.")) + 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 bool(nic_info["net-id"]) == bool(nic_info["port-id"]): - msg = _("either network or port should be specified " - "but not both") + msg = _( + 'Either network or port should be specified ' + 'but not both' + ) 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"]: @@ -1034,17 +1144,22 @@ class CreateServer(command.ShowOne): nic_info["net-id"] )['id'] if nic_info["port-id"]: - msg = _("can't create server with port specified " - "since network endpoint not enabled") + 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.') + 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: @@ -1105,6 +1220,7 @@ class CreateServer(command.ShowOne): userdata=userdata, key_name=parsed_args.key_name, availability_zone=parsed_args.availability_zone, + admin_pass=parsed_args.password, block_device_mapping_v2=block_device_mapping_v2, nics=nics, scheduler_hints=hints, @@ -1113,18 +1229,34 @@ class CreateServer(command.ShowOne): if parsed_args.description: boot_kwargs['description'] = parsed_args.description + if parsed_args.tags: + if compute_client.api_version < api_versions.APIVersion('2.52'): + msg = _( + '--os-compute-api-version 2.52 or greater is required to ' + 'support the --tag option' + ) + raise exceptions.CommandError(msg) + + boot_kwargs['tags'] = parsed_args.tags + if parsed_args.host: if compute_client.api_version < api_versions.APIVersion("2.74"): - msg = _("Specifying --host is not supported for " - "--os-compute-api-version less than 2.74") + msg = _( + '--os-compute-api-version 2.74 or greater is required to ' + 'support the --host option' + ) raise exceptions.CommandError(msg) + boot_kwargs['host'] = parsed_args.host if parsed_args.hypervisor_hostname: if compute_client.api_version < api_versions.APIVersion("2.74"): - msg = _("Specifying --hypervisor-hostname is not supported " - "for --os-compute-api-version less than 2.74") + msg = _( + '--os-compute-api-version 2.74 or greater is required to ' + 'support the --hypervisor-hostname option' + ) raise exceptions.CommandError(msg) + boot_kwargs['hypervisor_hostname'] = ( parsed_args.hypervisor_hostname) @@ -1150,8 +1282,7 @@ class CreateServer(command.ShowOne): ): self.app.stdout.write('\n') else: - LOG.error(_('Error creating server: %s'), - parsed_args.server_name) + LOG.error('Error creating server: %s', parsed_args.server_name) self.app.stdout.write(_('Error creating server\n')) raise SystemExit @@ -1380,6 +1511,30 @@ class ListServer(command.Lister): help=_('Only display unlocked servers. ' 'Requires ``--os-compute-api-version`` 2.73 or greater.'), ) + parser.add_argument( + '--tags', + metavar='<tag>', + action='append', + default=[], + dest='tags', + help=_( + 'Only list servers with the specified tag. ' + 'Specify multiple times to filter on multiple tags. ' + '(supported by --os-compute-api-version 2.26 or above)' + ), + ) + parser.add_argument( + '--not-tags', + metavar='<tag>', + action='append', + default=[], + dest='not_tags', + help=_( + 'Only list servers without the specified tag. ' + 'Specify multiple times to filter on multiple tags. ' + '(supported by --os-compute-api-version 2.26 or above)' + ), + ) return parser def take_action(self, parsed_args): @@ -1435,6 +1590,27 @@ class ListServer(command.Lister): 'changes-before': parsed_args.changes_before, 'changes-since': parsed_args.changes_since, } + + if parsed_args.tags: + if compute_client.api_version < api_versions.APIVersion('2.26'): + msg = _( + '--os-compute-api-version 2.26 or greater is required to ' + 'support the --tag option' + ) + raise exceptions.CommandError(msg) + + search_opts['tags'] = parsed_args.tags + + if parsed_args.not_tags: + if compute_client.api_version < api_versions.APIVersion('2.26'): + msg = _( + '--os-compute-api-version 2.26 or greater is required to ' + 'support the --not-tag option' + ) + raise exceptions.CommandError(msg) + + search_opts['not-tags'] = parsed_args.not_tags + support_locked = (compute_client.api_version >= api_versions.APIVersion('2.73')) if not support_locked and (parsed_args.locked or parsed_args.unlocked): @@ -2803,6 +2979,18 @@ class SetServer(command.Command): help=_('New server description (supported by ' '--os-compute-api-version 2.19 or above)'), ) + parser.add_argument( + '--tag', + metavar='<tag>', + action='append', + default=[], + dest='tags', + help=_( + 'Tag for the server. ' + 'Specify multiple times to add multiple tags. ' + '(supported by --os-compute-api-version 2.26 or above)' + ), + ) return parser def take_action(self, parsed_args): @@ -2841,6 +3029,17 @@ class SetServer(command.Command): raise exceptions.CommandError(msg) server.update(description=parsed_args.description) + if parsed_args.tags: + if server.api_version < api_versions.APIVersion('2.26'): + msg = _( + '--os-compute-api-version 2.26 or greater is required to ' + 'support the --tag option' + ) + raise exceptions.CommandError(msg) + + for tag in parsed_args.tags: + server.add_tag(tag=tag) + class ShelveServer(command.Command): _description = _("Shelve server(s)") @@ -3182,7 +3381,7 @@ class UnrescueServer(command.Command): class UnsetServer(command.Command): - _description = _("Unset server properties") + _description = _("Unset server properties and tags") def get_parser(self, prog_name): parser = super(UnsetServer, self).get_parser(prog_name) @@ -3206,6 +3405,18 @@ class UnsetServer(command.Command): help=_('Unset server description (supported by ' '--os-compute-api-version 2.19 or above)'), ) + parser.add_argument( + '--tag', + metavar='<tag>', + action='append', + default=[], + dest='tags', + help=_( + 'Tag to remove from the server. ' + 'Specify multiple times to remove multiple tags. ' + '(supported by --os-compute-api-version 2.26 or later' + ), + ) return parser def take_action(self, parsed_args): @@ -3231,6 +3442,17 @@ class UnsetServer(command.Command): description="", ) + if parsed_args.tags: + if compute_client.api_version < api_versions.APIVersion('2.26'): + msg = _( + '--os-compute-api-version 2.26 or greater is required to ' + 'support the --tag option' + ) + raise exceptions.CommandError(msg) + + for tag in parsed_args.tags: + compute_client.servers.delete_tag(server, tag=tag) + class UnshelveServer(command.Command): _description = _("Unshelve server(s)") diff --git a/openstackclient/compute/v2/server_group.py b/openstackclient/compute/v2/server_group.py index 1af6e28d..a3363244 100644 --- a/openstackclient/compute/v2/server_group.py +++ b/openstackclient/compute/v2/server_group.py @@ -56,12 +56,18 @@ class CreateServerGroup(command.ShowOne): parser.add_argument( '--policy', metavar='<policy>', + choices=[ + 'affinity', + 'anti-affinity', + 'soft-affinity', + 'soft-anti-affinity', + ], default='affinity', - help=_("Add a policy to <name> " - "('affinity' or 'anti-affinity', " - "defaults to 'affinity'). Specify --os-compute-api-version " - "2.15 or higher for the 'soft-affinity' or " - "'soft-anti-affinity' policy.") + help=_( + "Add a policy to <name> " + "Specify --os-compute-api-version 2.15 or higher for the " + "'soft-affinity' or 'soft-anti-affinity' policy." + ) ) return parser @@ -69,9 +75,18 @@ class CreateServerGroup(command.ShowOne): compute_client = self.app.client_manager.compute info = {} + if parsed_args.policy in ('soft-affinity', 'soft-anti-affinity'): + if compute_client.api_version < api_versions.APIVersion('2.15'): + msg = _( + '--os-compute-api-version 2.15 or greater is required to ' + 'support the %s policy' + ) + raise exceptions.CommandError(msg % parsed_args.policy) + policy_arg = {'policies': [parsed_args.policy]} if compute_client.api_version >= api_versions.APIVersion("2.64"): policy_arg = {'policy': parsed_args.policy} + server_group = compute_client.server_groups.create( name=parsed_args.name, **policy_arg) diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 029f57a3..4f3e9d0b 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -354,7 +354,7 @@ class CreateImage(command.ShowOne): # Build an attribute dict from the parsed args, only include # attributes that were actually set on the command line - kwargs = {} + kwargs = {'allow_duplicates': True} copy_attrs = ('name', 'id', 'container_format', 'disk_format', 'min_disk', 'min_ram', 'tags', 'visibility') diff --git a/openstackclient/network/v2/security_group_rule.py b/openstackclient/network/v2/security_group_rule.py index 1fbd97ab..a7703933 100644 --- a/openstackclient/network/v2/security_group_rule.py +++ b/openstackclient/network/v2/security_group_rule.py @@ -474,7 +474,7 @@ class ListSecurityGroupRule(common.NetworkAndComputeLister): action='store_true', default=False, help=self.enhance_help_neutron( - _("List additional fields in output")) + _("**Deprecated** This argument is no longer needed")) ) return parser @@ -504,15 +504,19 @@ class ListSecurityGroupRule(common.NetworkAndComputeLister): 'Ethertype', 'IP Range', 'Port Range', + 'Direction', + 'Remote Security Group', ) - if parsed_args.long: - column_headers = column_headers + ('Direction',) - column_headers = column_headers + ('Remote Security Group',) if parsed_args.group is None: column_headers = column_headers + ('Security Group',) return column_headers def take_action_network(self, client, parsed_args): + if parsed_args.long: + self.log.warning(_( + "The --long option has been deprecated and is no longer needed" + )) + column_headers = self._get_column_headers(parsed_args) columns = ( 'id', @@ -520,10 +524,9 @@ class ListSecurityGroupRule(common.NetworkAndComputeLister): 'ether_type', 'remote_ip_prefix', 'port_range', + 'direction', + 'remote_group_id', ) - if parsed_args.long: - columns = columns + ('direction',) - columns = columns + ('remote_group_id',) # Get the security group rules using the requested query. query = {} diff --git a/openstackclient/tests/functional/compute/v2/test_flavor.py b/openstackclient/tests/functional/compute/v2/test_flavor.py index c274adf2..162d4287 100644 --- a/openstackclient/tests/functional/compute/v2/test_flavor.py +++ b/openstackclient/tests/functional/compute/v2/test_flavor.py @@ -115,8 +115,8 @@ class FlavorTests(base.TestCase): self.assertFalse( cmd_output["os-flavor-access:is_public"], ) - self.assertEqual( - "a='b2', b='d2'", + self.assertDictEqual( + {"a": "b2", "b": "d2"}, cmd_output["properties"], ) @@ -133,12 +133,18 @@ class FlavorTests(base.TestCase): "flavor list -f json " + "--long" )) - col_name = [x["Name"] for x in cmd_output] - col_properties = [x['Properties'] for x in cmd_output] - self.assertIn(name1, col_name) - self.assertIn("a='b', c='d'", col_properties) - self.assertNotIn(name2, col_name) - self.assertNotIn("b2', b='d2'", col_properties) + # We have list of complex json objects + # Iterate through the list setting flags + found_expected = False + for rec in cmd_output: + if rec['Name'] == name1: + found_expected = True + self.assertEqual('b', rec['Properties']['a']) + self.assertEqual('d', rec['Properties']['c']) + elif rec['Name'] == name2: + # We should have not seen private flavor + self.assertFalse(True) + self.assertTrue(found_expected) # Test list --public cmd_output = json.loads(self.openstack( @@ -201,8 +207,8 @@ class FlavorTests(base.TestCase): self.assertFalse( cmd_output["os-flavor-access:is_public"], ) - self.assertEqual( - "a='first', b='second'", + self.assertDictEqual( + {"a": "first", "b": "second"}, cmd_output["properties"], ) @@ -223,9 +229,14 @@ class FlavorTests(base.TestCase): cmd_output["id"], ) self.assertEqual( - "a='third and 10', b='second', g='fourth'", - cmd_output['properties'], - ) + 'third and 10', + cmd_output['properties']['a']) + self.assertEqual( + 'second', + cmd_output['properties']['b']) + self.assertEqual( + 'fourth', + cmd_output['properties']['g']) raw_output = self.openstack( "flavor unset " + @@ -238,7 +249,5 @@ class FlavorTests(base.TestCase): "flavor show -f json " + name1 )) - self.assertEqual( - "a='third and 10', g='fourth'", - cmd_output["properties"], - ) + + self.assertNotIn('b', cmd_output['properties']) diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index ce556fa6..3a06d271 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -884,6 +884,7 @@ class FakeKeypair(object): # Set default attributes. keypair_info = { 'name': 'keypair-name-' + uuid.uuid4().hex, + 'type': 'ssh', 'fingerprint': 'dummy', 'public_key': 'dummy', 'user_id': 'user' diff --git a/openstackclient/tests/unit/compute/v2/test_console.py b/openstackclient/tests/unit/compute/v2/test_console.py index 99a14f04..db9603c9 100644 --- a/openstackclient/tests/unit/compute/v2/test_console.py +++ b/openstackclient/tests/unit/compute/v2/test_console.py @@ -17,29 +17,103 @@ from unittest import mock from openstackclient.compute.v2 import console from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes +from openstackclient.tests.unit import utils class TestConsole(compute_fakes.TestComputev2): def setUp(self): super(TestConsole, self).setUp() - self.servers_mock = self.app.client_manager.compute.servers - self.servers_mock.reset_mock() + + # SDK mock + self.app.client_manager.sdk_connection = mock.Mock() + self.app.client_manager.sdk_connection.compute = mock.Mock() + self.sdk_client = self.app.client_manager.sdk_connection.compute + self.sdk_client.find_server = mock.Mock() + self.sdk_client.get_server_console_output = mock.Mock() + + +class TestConsoleLog(TestConsole): + _server = compute_fakes.FakeServer.create_one_server() + + def setUp(self): + super(TestConsoleLog, self).setUp() + + self.sdk_client.find_server.return_value = self._server + + self.cmd = console.ShowConsoleLog(self.app, None) + + def test_show_no_args(self): + arglist = [ + ] + verifylist = [ + ] + self.assertRaises(utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist) + + def test_show(self): + arglist = [ + 'fake_server' + ] + verifylist = [ + ('server', 'fake_server') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + output = { + 'output': '1st line\n2nd line\n' + } + self.sdk_client.get_server_console_output.return_value = output + self.cmd.take_action(parsed_args) + + self.sdk_client.find_server.assert_called_with( + name_or_id='fake_server', ignore_missing=False) + self.sdk_client.get_server_console_output.assert_called_with( + self._server.id, + length=None + ) + stdout = self.app.stdout.content + self.assertEqual(stdout[0], output['output']) + + def test_show_lines(self): + arglist = [ + 'fake_server', + '--lines', '15' + ] + verifylist = [ + ('server', 'fake_server'), + ('lines', 15) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + output = { + 'output': '1st line\n2nd line' + } + self.sdk_client.get_server_console_output.return_value = output + self.cmd.take_action(parsed_args) + + self.sdk_client.find_server.assert_called_with( + name_or_id='fake_server', ignore_missing=False) + self.sdk_client.get_server_console_output.assert_called_with( + self._server.id, + length=15 + ) class TestConsoleUrlShow(TestConsole): + _server = compute_fakes.FakeServer.create_one_server() def setUp(self): super(TestConsoleUrlShow, self).setUp() - fake_console_data = {'remote_console': {'url': 'http://localhost', - 'protocol': 'fake_protocol', - 'type': 'fake_type'}} - methods = { - 'get_console_url': fake_console_data - } - self.fake_server = compute_fakes.FakeServer.create_one_server( - methods=methods) - self.servers_mock.get.return_value = self.fake_server + self.sdk_client.find_server.return_value = self._server + fake_console_data = {'url': 'http://localhost', + 'protocol': 'fake_protocol', + 'type': 'fake_type'} + self.sdk_client.create_console = mock.Mock( + return_value=fake_console_data) self.columns = ( 'protocol', @@ -47,9 +121,9 @@ class TestConsoleUrlShow(TestConsole): 'url', ) self.data = ( - fake_console_data['remote_console']['protocol'], - fake_console_data['remote_console']['type'], - fake_console_data['remote_console']['url'] + fake_console_data['protocol'], + fake_console_data['type'], + fake_console_data['url'] ) self.cmd = console.ShowConsoleURL(self.app, None) @@ -64,7 +138,9 @@ class TestConsoleUrlShow(TestConsole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.fake_server.get_console_url.assert_called_once_with('novnc') + self.sdk_client.create_console.assert_called_once_with( + self._server.id, + console_type='novnc') self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -79,7 +155,9 @@ class TestConsoleUrlShow(TestConsole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.fake_server.get_console_url.assert_called_once_with('novnc') + self.sdk_client.create_console.assert_called_once_with( + self._server.id, + console_type='novnc') self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -94,7 +172,9 @@ class TestConsoleUrlShow(TestConsole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.fake_server.get_console_url.assert_called_once_with('xvpvnc') + self.sdk_client.create_console.assert_called_once_with( + self._server.id, + console_type='xvpvnc') self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -109,41 +189,12 @@ class TestConsoleUrlShow(TestConsole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.fake_server.get_console_url.assert_called_once_with( - 'spice-html5') + self.sdk_client.create_console.assert_called_once_with( + self._server.id, + console_type='spice-html5') self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) - def test_console_url_show_compatible(self): - methods = { - 'get_console_url': {'console': {'url': 'http://localhost', - 'type': 'fake_type'}}, - } - old_fake_server = compute_fakes.FakeServer.create_one_server( - methods=methods) - old_columns = ( - 'type', - 'url', - ) - old_data = ( - methods['get_console_url']['console']['type'], - methods['get_console_url']['console']['url'] - ) - arglist = [ - 'foo_vm', - ] - verifylist = [ - ('url_type', 'novnc'), - ('server', 'foo_vm'), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - with mock.patch.object(self.servers_mock, 'get', - return_value=old_fake_server): - columns, data = self.cmd.take_action(parsed_args) - old_fake_server.get_console_url.assert_called_once_with('novnc') - self.assertEqual(old_columns, columns) - self.assertEqual(old_data, data) - def test_console_url_show_with_rdp(self): arglist = [ '--rdp', @@ -155,8 +206,9 @@ class TestConsoleUrlShow(TestConsole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.fake_server.get_console_url.assert_called_once_with( - 'rdp-html5') + self.sdk_client.create_console.assert_called_once_with( + self._server.id, + console_type='rdp-html5') self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -171,8 +223,9 @@ class TestConsoleUrlShow(TestConsole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.fake_server.get_console_url.assert_called_once_with( - 'serial') + self.sdk_client.create_console.assert_called_once_with( + self._server.id, + console_type='serial') self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -187,6 +240,8 @@ class TestConsoleUrlShow(TestConsole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.fake_server.get_console_url.assert_called_once_with('webmks') + self.sdk_client.create_console.assert_called_once_with( + self._server.id, + console_type='webmks') self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/compute/v2/test_flavor.py b/openstackclient/tests/unit/compute/v2/test_flavor.py index 4732cc82..2828d74e 100644 --- a/openstackclient/tests/unit/compute/v2/test_flavor.py +++ b/openstackclient/tests/unit/compute/v2/test_flavor.py @@ -17,8 +17,8 @@ from unittest import mock from unittest.mock import call import novaclient +from osc_lib.cli import format_columns from osc_lib import exceptions -from osc_lib import utils from openstackclient.compute.v2 import flavor from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes @@ -70,7 +70,7 @@ class TestFlavorCreate(TestFlavor): flavor.id, flavor.name, flavor.is_public, - utils.format_dict(flavor.properties), + format_columns.DictColumn(flavor.properties), flavor.ram, flavor.rxtx_factor, flavor.swap, @@ -111,7 +111,7 @@ class TestFlavorCreate(TestFlavor): self.flavors_mock.create.assert_called_once_with(*default_args) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_flavor_create_all_options(self): @@ -165,7 +165,7 @@ class TestFlavorCreate(TestFlavor): self.flavor.get_keys.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_flavor_create_other_options(self): @@ -226,7 +226,7 @@ class TestFlavorCreate(TestFlavor): {'key1': 'value1', 'key2': 'value2'}) self.flavor.get_keys.assert_called_with() self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_public_flavor_create_with_project(self): arglist = [ @@ -300,7 +300,7 @@ class TestFlavorCreate(TestFlavor): self.flavors_mock.create.assert_called_once_with(*args) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_flavor_create_with_description_api_older(self): arglist = [ @@ -429,7 +429,7 @@ class TestFlavorList(TestFlavor): data_long = (data[0] + ( flavors[0].swap, flavors[0].rxtx_factor, - u'property=\'value\'' + format_columns.DictColumn(flavors[0].properties) ), ) def setUp(self): @@ -583,7 +583,7 @@ class TestFlavorList(TestFlavor): ) self.assertEqual(self.columns_long, columns) - self.assertEqual(tuple(self.data_long), tuple(data)) + self.assertListItemEqual(self.data_long, tuple(data)) class TestFlavorSet(TestFlavor): @@ -817,7 +817,7 @@ class TestFlavorShow(TestFlavor): flavor.id, flavor.name, flavor.is_public, - utils.format_dict(flavor.get_keys()), + format_columns.DictColumn(flavor.get_keys()), flavor.ram, flavor.rxtx_factor, flavor.swap, @@ -854,7 +854,7 @@ class TestFlavorShow(TestFlavor): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_private_flavor_show(self): private_flavor = compute_fakes.FakeFlavor.create_one_flavor( @@ -874,13 +874,13 @@ class TestFlavorShow(TestFlavor): data_with_project = ( private_flavor.disabled, private_flavor.ephemeral, - self.flavor_access.tenant_id, + [self.flavor_access.tenant_id], private_flavor.description, private_flavor.disk, private_flavor.id, private_flavor.name, private_flavor.is_public, - utils.format_dict(private_flavor.get_keys()), + format_columns.DictColumn(private_flavor.get_keys()), private_flavor.ram, private_flavor.rxtx_factor, private_flavor.swap, @@ -894,7 +894,7 @@ class TestFlavorShow(TestFlavor): self.flavor_access_mock.list.assert_called_with( flavor=private_flavor.id) self.assertEqual(self.columns, columns) - self.assertEqual(data_with_project, data) + self.assertItemEqual(data_with_project, data) class TestFlavorUnset(TestFlavor): diff --git a/openstackclient/tests/unit/compute/v2/test_keypair.py b/openstackclient/tests/unit/compute/v2/test_keypair.py index 1f3f56f9..286cbb09 100644 --- a/openstackclient/tests/unit/compute/v2/test_keypair.py +++ b/openstackclient/tests/unit/compute/v2/test_keypair.py @@ -13,15 +13,19 @@ # under the License. # +import copy from unittest import mock from unittest.mock import call import uuid +from novaclient import api_versions from osc_lib import exceptions from osc_lib import utils from openstackclient.compute.v2 import keypair from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes +from openstackclient.tests.unit import fakes +from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes from openstackclient.tests.unit import utils as tests_utils @@ -45,11 +49,13 @@ class TestKeypairCreate(TestKeypair): self.columns = ( 'fingerprint', 'name', + 'type', 'user_id' ) self.data = ( self.keypair.fingerprint, self.keypair.name, + self.keypair.type, self.keypair.user_id ) @@ -71,7 +77,7 @@ class TestKeypairCreate(TestKeypair): columns, data = self.cmd.take_action(parsed_args) self.keypairs_mock.create.assert_called_with( - self.keypair.name, + name=self.keypair.name, public_key=None ) @@ -87,6 +93,7 @@ class TestKeypairCreate(TestKeypair): self.data = ( self.keypair.fingerprint, self.keypair.name, + self.keypair.type, self.keypair.user_id ) @@ -96,7 +103,7 @@ class TestKeypairCreate(TestKeypair): ] verifylist = [ ('public_key', self.keypair.public_key), - ('name', self.keypair.name) + ('name', self.keypair.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -109,8 +116,8 @@ class TestKeypairCreate(TestKeypair): columns, data = self.cmd.take_action(parsed_args) self.keypairs_mock.create.assert_called_with( - self.keypair.name, - public_key=self.keypair.public_key + name=self.keypair.name, + public_key=self.keypair.public_key, ) self.assertEqual(self.columns, columns) @@ -124,7 +131,7 @@ class TestKeypairCreate(TestKeypair): ] verifylist = [ ('private_key', tmp_pk_file), - ('name', self.keypair.name) + ('name', self.keypair.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -136,8 +143,8 @@ class TestKeypairCreate(TestKeypair): columns, data = self.cmd.take_action(parsed_args) self.keypairs_mock.create.assert_called_with( - self.keypair.name, - public_key=None + name=self.keypair.name, + public_key=None, ) mock_open.assert_called_once_with(tmp_pk_file, 'w+') @@ -146,6 +153,79 @@ class TestKeypairCreate(TestKeypair): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + def test_keypair_create_with_key_type(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.2') + + for key_type in ['x509', 'ssh']: + self.keypair = compute_fakes.FakeKeypair.create_one_keypair( + no_pri=True) + self.keypairs_mock.create.return_value = self.keypair + + self.data = ( + self.keypair.fingerprint, + self.keypair.name, + self.keypair.type, + self.keypair.user_id, + ) + arglist = [ + '--public-key', self.keypair.public_key, + self.keypair.name, + '--type', key_type, + ] + verifylist = [ + ('public_key', self.keypair.public_key), + ('name', self.keypair.name), + ('type', key_type), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch('io.open') as mock_open: + mock_open.return_value = mock.MagicMock() + m_file = mock_open.return_value.__enter__.return_value + m_file.read.return_value = 'dummy' + columns, data = self.cmd.take_action(parsed_args) + + self.keypairs_mock.create.assert_called_with( + name=self.keypair.name, + public_key=self.keypair.public_key, + key_type=key_type, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_keypair_create_with_key_type_pre_v22(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.1') + + for key_type in ['x509', 'ssh']: + arglist = [ + '--public-key', self.keypair.public_key, + self.keypair.name, + '--type', 'ssh', + ] + verifylist = [ + ('public_key', self.keypair.public_key), + ('name', self.keypair.name), + ('type', 'ssh'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch('io.open') as mock_open: + mock_open.return_value = mock.MagicMock() + m_file = mock_open.return_value.__enter__.return_value + m_file.read.return_value = 'dummy' + + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + self.assertIn( + '--os-compute-api-version 2.2 or greater is required', + str(ex)) + class TestKeypairDelete(TestKeypair): @@ -227,16 +307,6 @@ class TestKeypairList(TestKeypair): # Return value of self.keypairs_mock.list(). keypairs = compute_fakes.FakeKeypair.create_keypairs(count=1) - columns = ( - "Name", - "Fingerprint" - ) - - data = (( - keypairs[0].name, - keypairs[0].fingerprint - ), ) - def setUp(self): super(TestKeypairList, self).setUp() @@ -247,8 +317,7 @@ class TestKeypairList(TestKeypair): def test_keypair_list_no_options(self): arglist = [] - verifylist = [ - ] + verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -261,8 +330,175 @@ class TestKeypairList(TestKeypair): self.keypairs_mock.list.assert_called_with() - self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertEqual(('Name', 'Fingerprint'), columns) + self.assertEqual( + ((self.keypairs[0].name, self.keypairs[0].fingerprint), ), + tuple(data) + ) + + def test_keypair_list_v22(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.2') + + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + + self.keypairs_mock.list.assert_called_with() + + self.assertEqual(('Name', 'Fingerprint', 'Type'), columns) + self.assertEqual( + (( + self.keypairs[0].name, + self.keypairs[0].fingerprint, + self.keypairs[0].type, + ), ), + tuple(data) + ) + + def test_keypair_list_with_user(self): + + # Filtering by user is support for nova api 2.10 or above + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.10') + + users_mock = self.app.client_manager.identity.users + users_mock.reset_mock() + users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + + arglist = [ + '--user', identity_fakes.user_name, + ] + verifylist = [ + ('user', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + users_mock.get.assert_called_with(identity_fakes.user_name) + self.keypairs_mock.list.assert_called_with( + user_id=identity_fakes.user_id, + ) + + self.assertEqual(('Name', 'Fingerprint', 'Type'), columns) + self.assertEqual( + (( + self.keypairs[0].name, + self.keypairs[0].fingerprint, + self.keypairs[0].type, + ), ), + tuple(data) + ) + + def test_keypair_list_with_user_pre_v210(self): + + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.9') + + arglist = [ + '--user', identity_fakes.user_name, + ] + verifylist = [ + ('user', identity_fakes.user_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.10 or greater is required', str(ex)) + + def test_keypair_list_with_project(self): + + # Filtering by user is support for nova api 2.10 or above + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.10') + + projects_mock = self.app.client_manager.identity.tenants + projects_mock.reset_mock() + projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + + users_mock = self.app.client_manager.identity.users + users_mock.reset_mock() + users_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ), + ] + + arglist = ['--project', identity_fakes.project_name] + verifylist = [('project', identity_fakes.project_name)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + projects_mock.get.assert_called_with(identity_fakes.project_name) + users_mock.list.assert_called_with(tenant_id=identity_fakes.project_id) + self.keypairs_mock.list.assert_called_with( + user_id=identity_fakes.user_id, + ) + + self.assertEqual(('Name', 'Fingerprint', 'Type'), columns) + self.assertEqual( + (( + self.keypairs[0].name, + self.keypairs[0].fingerprint, + self.keypairs[0].type, + ), ), + tuple(data) + ) + + def test_keypair_list_with_project_pre_v210(self): + + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.9') + + arglist = ['--project', identity_fakes.project_name] + verifylist = [('project', identity_fakes.project_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.10 or greater is required', str(ex)) + + def test_keypair_list_conflicting_user_options(self): + + # Filtering by user is support for nova api 2.10 or above + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.10') + + arglist = [ + '--user', identity_fakes.user_name, + '--project', identity_fakes.project_name, + ] + + self.assertRaises( + tests_utils.ParserException, + self.check_parser, self.cmd, arglist, None) class TestKeypairShow(TestKeypair): @@ -279,16 +515,18 @@ class TestKeypairShow(TestKeypair): self.columns = ( "fingerprint", "name", + "type", "user_id" ) self.data = ( self.keypair.fingerprint, self.keypair.name, + self.keypair.type, self.keypair.user_id ) - def test_show_no_options(self): + def test_keypair_show_no_options(self): arglist = [] verifylist = [] @@ -306,6 +544,7 @@ class TestKeypairShow(TestKeypair): self.data = ( self.keypair.fingerprint, self.keypair.name, + self.keypair.type, self.keypair.user_id ) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 51a3beb5..15cdbf16 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -186,6 +186,72 @@ class TestServerAddFixedIP(TestServer): extralist = ['--fixed-ip-address', '5.6.7.8'] self._test_server_add_fixed_ip(extralist, '5.6.7.8') + def test_server_add_fixed_ip_with_tag(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.49') + + servers = self.setup_servers_mock(count=1) + network = compute_fakes.FakeNetwork.create_one_network() + with mock.patch( + 'openstackclient.api.compute_v2.APIv2.network_find' + ) as net_mock: + net_mock.return_value = network + + arglist = [ + servers[0].id, + network['id'], + '--fixed-ip-address', '5.6.7.8', + '--tag', 'tag1', + ] + verifylist = [ + ('server', servers[0].id), + ('network', network['id']), + ('fixed_ip_address', '5.6.7.8'), + ('tag', 'tag1'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + servers[0].interface_attach.assert_called_once_with( + port_id=None, + net_id=network['id'], + fixed_ip='5.6.7.8', + tag='tag1' + ) + self.assertIsNone(result) + + def test_server_add_fixed_ip_with_tag_pre_v249(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.48') + + servers = self.setup_servers_mock(count=1) + network = compute_fakes.FakeNetwork.create_one_network() + with mock.patch( + 'openstackclient.api.compute_v2.APIv2.network_find' + ) as net_mock: + net_mock.return_value = network + + arglist = [ + servers[0].id, + network['id'], + '--fixed-ip-address', '5.6.7.8', + '--tag', 'tag1', + ] + verifylist = [ + ('server', servers[0].id), + ('network', network['id']), + ('fixed_ip_address', '5.6.7.8'), + ('tag', 'tag1'), + ] + 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.49 or greater is required', + str(ex)) + @mock.patch( 'openstackclient.api.compute_v2.APIv2.floating_ip_add' @@ -596,8 +662,57 @@ class TestServerVolume(TestServer): servers[0].id, self.volume.id, device='/dev/sdb') self.assertIsNone(result) + def test_server_add_volume_with_tag(self): + # requires API 2.49 or later + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.49') + + servers = self.setup_servers_mock(count=1) + arglist = [ + '--device', '/dev/sdb', + '--tag', 'foo', + servers[0].id, + self.volume.id, + ] + verifylist = [ + ('server', servers[0].id), + ('volume', self.volume.id), + ('device', '/dev/sdb'), + ('tag', 'foo'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.servers_volumes_mock.create_server_volume.assert_called_once_with( + servers[0].id, self.volume.id, device='/dev/sdb', tag='foo') + self.assertIsNone(result) + + def test_server_add_volume_with_tag_pre_v249(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.48') -class TestServerVolumeV279(TestServerVolume): + servers = self.setup_servers_mock(count=1) + arglist = [ + servers[0].id, + self.volume.id, + '--tag', 'foo', + ] + verifylist = [ + ('server', servers[0].id), + ('volume', self.volume.id), + ('tag', 'foo'), + ] + 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.49 or greater is required', + str(ex)) def test_server_add_volume_with_enable_delete_on_termination(self): self.app.client_manager.compute.api_version = api_versions.APIVersion( @@ -654,7 +769,8 @@ class TestServerVolumeV279(TestServerVolume): self.assertIsNone(result) def test_server_add_volume_with_enable_delete_on_termination_pre_v279( - self): + self, + ): self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.78') @@ -678,7 +794,8 @@ class TestServerVolumeV279(TestServerVolume): str(ex)) def test_server_add_volume_with_disable_delete_on_termination_pre_v279( - self): + self, + ): self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.78') @@ -702,7 +819,8 @@ class TestServerVolumeV279(TestServerVolume): str(ex)) def test_server_add_volume_with_disable_and_enable_delete_on_termination( - self): + self, + ): self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.79') @@ -981,6 +1099,7 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics=[], scheduler_hints={}, @@ -1007,6 +1126,7 @@ class TestServerCreate(TestServer): '--property', 'Beta=b', '--security-group', 'securitygroup', '--use-config-drive', + '--password', 'passw0rd', '--hint', 'a=b', '--hint', 'a=c', self.new_server.name, @@ -1019,6 +1139,7 @@ class TestServerCreate(TestServer): ('security_group', ['securitygroup']), ('hint', {'a': ['b', 'c']}), ('config_drive', True), + ('password', 'passw0rd'), ('server_name', self.new_server.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1047,6 +1168,7 @@ class TestServerCreate(TestServer): userdata=None, key_name='keyname', availability_zone=None, + admin_pass='passw0rd', block_device_mapping_v2=[], nics=[], scheduler_hints={'a': ['b', 'c']}, @@ -1133,6 +1255,7 @@ class TestServerCreate(TestServer): userdata=None, key_name='keyname', availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics=[], scheduler_hints={}, @@ -1219,6 +1342,7 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics=[{'net-id': 'net1_uuid', 'v4-fixed-ip': '', @@ -1283,6 +1407,7 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics='auto', scheduler_hints={}, @@ -1332,6 +1457,7 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics='auto', scheduler_hints={}, @@ -1377,6 +1503,7 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics='none', scheduler_hints={}, @@ -1542,6 +1669,7 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics=[], scheduler_hints={}, @@ -1592,6 +1720,7 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics=[], scheduler_hints={}, @@ -1647,6 +1776,7 @@ class TestServerCreate(TestServer): userdata=mock_file, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics=[], scheduler_hints={}, @@ -1693,6 +1823,7 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[{ 'device_name': 'vda', 'uuid': self.volume.id, @@ -1745,6 +1876,7 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[{ 'device_name': 'vdf', 'uuid': self.volume.id, @@ -1796,6 +1928,7 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[{ 'device_name': 'vdf', 'uuid': self.volume.id, @@ -1849,6 +1982,7 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[{ 'device_name': 'vde', 'uuid': self.volume.id, @@ -1904,6 +2038,7 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[{ 'device_name': 'vds', 'uuid': self.snapshot.id, @@ -1959,6 +2094,7 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[ { 'device_name': 'vdb', @@ -2095,6 +2231,7 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics='none', meta=None, @@ -2150,6 +2287,7 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics='none', meta=None, @@ -2239,6 +2377,7 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics='none', meta=None, @@ -2319,6 +2458,7 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics='auto', scheduler_hints={}, @@ -2364,6 +2504,87 @@ class TestServerCreate(TestServer): self.assertRaises(exceptions.CommandError, self.cmd.take_action, parsed_args) + def test_server_create_with_tag(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.52') + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--tag', 'tag1', + '--tag', 'tag2', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('tags', ['tag1', 'tag2']), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + 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, + 'block_device_mapping_v2': [], + 'admin_pass': None, + 'nics': 'auto', + 'scheduler_hints': {}, + 'config_drive': None, + 'tags': ['tag1', 'tag2'], + } + # 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) + self.assertFalse(self.images_mock.called) + self.assertFalse(self.flavors_mock.called) + + def test_server_create_with_tag_pre_v252(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.51') + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--tag', 'tag1', + '--tag', 'tag2', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('tags', ['tag1', 'tag2']), + ('config_drive', False), + ('server_name', self.new_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.52 or greater is required', + str(ex)) + def test_server_create_with_host_v274(self): # Explicit host is supported for nova api version 2.74 or above @@ -2403,6 +2624,7 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics='auto', scheduler_hints={}, @@ -2488,6 +2710,7 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics='auto', scheduler_hints={}, @@ -2575,6 +2798,7 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics='auto', scheduler_hints={}, @@ -3133,6 +3357,7 @@ class TestServerList(TestServer): self.search_opts['changes-before'] = '2016-03-05T06:27:59Z' self.search_opts['deleted'] = True + self.servers_mock.list.assert_called_with(**self.kwargs) self.assertEqual(self.columns, columns) @@ -3225,6 +3450,92 @@ class TestServerList(TestServer): 'UNKNOWN', '', '', '') self.assertEqual(expected_row, partial_server) + def test_server_list_with_tag(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.26') + + arglist = [ + '--tag', 'tag1', + '--tag', 'tag2', + ] + verifylist = [ + ('tags', ['tag1', 'tag2']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.search_opts['tags'] = ['tag1', 'tag2'] + + self.servers_mock.list.assert_called_with(**self.kwargs) + + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + def test_server_list_with_tag_pre_v225(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.25') + + arglist = [ + '--tag', 'tag1', + '--tag', 'tag2', + ] + verifylist = [ + ('tags', ['tag1', 'tag2']), + ] + + 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.26 or greater is required', + str(ex)) + + def test_server_list_with_not_tag(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.26') + + arglist = [ + '--not-tag', 'tag1', + '--not-tag', 'tag2', + ] + verifylist = [ + ('not_tags', ['tag1', 'tag2']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.search_opts['not-tags'] = ['tag1', 'tag2'] + + self.servers_mock.list.assert_called_with(**self.kwargs) + + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + def test_server_list_with_not_tag_pre_v226(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.25') + + arglist = [ + '--not-tag', 'tag1', + '--not-tag', 'tag2', + ] + verifylist = [ + ('not_tags', ['tag1', 'tag2']), + ] + + 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.26 or greater is required', + str(ex)) + class TestServerLock(TestServer): @@ -5332,6 +5643,8 @@ class TestServerSet(TestServer): 'update': None, 'reset_state': None, 'change_password': None, + 'add_tag': None, + 'set_tags': None, } self.fake_servers = self.setup_servers_mock(2) @@ -5472,6 +5785,50 @@ class TestServerSet(TestServer): self.assertRaises(exceptions.CommandError, self.cmd.take_action, parsed_args) + def test_server_set_with_tag(self): + self.fake_servers[0].api_version = api_versions.APIVersion('2.26') + + arglist = [ + '--tag', 'tag1', + '--tag', 'tag2', + 'foo_vm', + ] + verifylist = [ + ('tags', ['tag1', 'tag2']), + ('server', 'foo_vm'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.fake_servers[0].add_tag.assert_has_calls([ + mock.call(tag='tag1'), + mock.call(tag='tag2'), + ]) + self.assertIsNone(result) + + def test_server_set_with_tag_pre_v226(self): + self.fake_servers[0].api_version = api_versions.APIVersion('2.25') + + arglist = [ + '--tag', 'tag1', + '--tag', 'tag2', + 'foo_vm', + ] + verifylist = [ + ('tags', ['tag1', 'tag2']), + ('server', 'foo_vm'), + ] + 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.26 or greater is required', + str(ex)) + class TestServerShelve(TestServer): @@ -5797,6 +6154,52 @@ class TestServerUnset(TestServer): self.assertRaises(exceptions.CommandError, self.cmd.take_action, parsed_args) + def test_server_unset_with_tag(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.26') + + arglist = [ + '--tag', 'tag1', + '--tag', 'tag2', + 'foo_vm', + ] + verifylist = [ + ('tags', ['tag1', 'tag2']), + ('server', 'foo_vm'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.servers_mock.delete_tag.assert_has_calls([ + mock.call(self.fake_server, tag='tag1'), + mock.call(self.fake_server, tag='tag2'), + ]) + + def test_server_unset_with_tag_pre_v226(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.25') + + arglist = [ + '--tag', 'tag1', + '--tag', 'tag2', + 'foo_vm', + ] + verifylist = [ + ('tags', ['tag1', 'tag2']), + ('server', 'foo_vm'), + ] + 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.26 or greater is required', + str(ex)) + class TestServerUnshelve(TestServer): diff --git a/openstackclient/tests/unit/compute/v2/test_server_group.py b/openstackclient/tests/unit/compute/v2/test_server_group.py index 359cd2bd..bf0ea0ba 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_group.py +++ b/openstackclient/tests/unit/compute/v2/test_server_group.py @@ -91,6 +91,28 @@ class TestServerGroupCreate(TestServerGroup): def test_server_group_create(self): arglist = [ + '--policy', 'anti-affinity', + 'affinity_group', + ] + verifylist = [ + ('policy', 'anti-affinity'), + ('name', 'affinity_group'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.server_groups_mock.create.assert_called_once_with( + name=parsed_args.name, + policies=[parsed_args.policy], + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_server_group_create_with_soft_policies(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.15') + + arglist = [ '--policy', 'soft-anti-affinity', 'affinity_group', ] @@ -108,6 +130,27 @@ class TestServerGroupCreate(TestServerGroup): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + def test_server_group_create_with_soft_policies_pre_v215(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.14') + + arglist = [ + '--policy', 'soft-anti-affinity', + 'affinity_group', + ] + verifylist = [ + ('policy', 'soft-anti-affinity'), + ('name', 'affinity_group'), + ] + 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.15 or greater is required', + str(ex)) + def test_server_group_create_v264(self): self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.64') diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index 310f6b76..b094817e 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -100,6 +100,7 @@ class TestImageCreate(TestImage): # ImageManager.create(name=, **) self.client.create_image.assert_called_with( name=self.new_image.name, + allow_duplicates=True, container_format=image.DEFAULT_CONTAINER_FORMAT, disk_format=image.DEFAULT_DISK_FORMAT, ) @@ -152,6 +153,7 @@ class TestImageCreate(TestImage): # ImageManager.create(name=, **) self.client.create_image.assert_called_with( name=self.new_image.name, + allow_duplicates=True, container_format='ovf', disk_format='ami', min_disk=10, @@ -239,6 +241,7 @@ class TestImageCreate(TestImage): # ImageManager.create(name=, **) self.client.create_image.assert_called_with( name=self.new_image.name, + allow_duplicates=True, container_format=image.DEFAULT_CONTAINER_FORMAT, disk_format=image.DEFAULT_DISK_FORMAT, is_protected=self.new_image.is_protected, @@ -246,7 +249,7 @@ class TestImageCreate(TestImage): Alpha='1', Beta='2', tags=self.new_image.tags, - filename=imagefile.name + filename=imagefile.name, ) self.assertEqual( @@ -288,6 +291,7 @@ class TestImageCreate(TestImage): # ImageManager.create(name=, **) self.client.create_image.assert_called_with( name=self.new_image.name, + allow_duplicates=True, container_format=image.DEFAULT_CONTAINER_FORMAT, disk_format=image.DEFAULT_DISK_FORMAT, use_import=True diff --git a/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py b/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py index 5720e305..b7e38afb 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py @@ -362,6 +362,7 @@ class TestListSecurityGroupRuleCompute(TestSecurityGroupRuleCompute): 'Ethertype', 'IP Range', 'Port Range', + 'Direction', 'Remote Security Group', ) expected_columns_no_group = \ diff --git a/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py b/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py index 0a9522b0..01411611 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py @@ -870,7 +870,7 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): _security_group_rules = [_security_group_rule_tcp, _security_group_rule_icmp] - expected_columns_with_group_and_long = ( + expected_columns_with_group = ( 'ID', 'IP Protocol', 'Ethertype', @@ -885,14 +885,15 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): 'Ethertype', 'IP Range', 'Port Range', + 'Direction', 'Remote Security Group', 'Security Group', ) - expected_data_with_group_and_long = [] + expected_data_with_group = [] expected_data_no_group = [] for _security_group_rule in _security_group_rules: - expected_data_with_group_and_long.append(( + expected_data_with_group.append(( _security_group_rule.id, _security_group_rule.protocol, _security_group_rule.ether_type, @@ -909,6 +910,7 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): _security_group_rule.remote_ip_prefix, security_group_rule._format_network_port_range( _security_group_rule), + _security_group_rule.direction, _security_group_rule.remote_group_id, _security_group_rule.security_group_id, )) @@ -935,14 +937,12 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): self.assertEqual(self.expected_columns_no_group, columns) self.assertEqual(self.expected_data_no_group, list(data)) - def test_list_with_group_and_long(self): + def test_list_with_group(self): self._security_group_rule_tcp.port_range_min = 80 arglist = [ - '--long', self._security_group.id, ] verifylist = [ - ('long', True), ('group', self._security_group.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -952,8 +952,8 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): self.network.security_group_rules.assert_called_once_with(**{ 'security_group_id': self._security_group.id, }) - self.assertEqual(self.expected_columns_with_group_and_long, columns) - self.assertEqual(self.expected_data_with_group_and_long, list(data)) + self.assertEqual(self.expected_columns_with_group, columns) + self.assertEqual(self.expected_data_with_group, list(data)) def test_list_with_ignored_options(self): self._security_group_rule_tcp.port_range_min = 80 |
