diff options
20 files changed, 845 insertions, 206 deletions
diff --git a/doc/source/cli/data/glance.csv b/doc/source/cli/data/glance.csv index d5c65f2d..adca8c0e 100644 --- a/doc/source/cli/data/glance.csv +++ b/doc/source/cli/data/glance.csv @@ -1,6 +1,10 @@ +cache-clear,,"Clear all images from cache, queue or both." +cache-delete,,Delete image from cache/caching queue. +cache-list,,Get cache state. +cache-queue,,Queue image(s) for caching. explain,WONTFIX,Describe a specific model. image-create,image create,Create a new image. -image-create-via-import,,EXPERIMENTAL: Create a new image via image import. +image-create-via-import, image create --import,"EXPERIMENTAL: Create a new image via image import using glance-direct import method. Missing support for web-download, copy-image and glance-download import methods. The OSC command is also missing support for importing image to specified store as well as all stores (--store, --stores, --all-stores) and skip or stop processing if import fails to one of the store (--allow-failure)" image-deactivate,image set --deactivate,Deactivate specified image. image-delete,image delete,Delete specified image. image-download,image save,Download a specific image. @@ -11,6 +15,7 @@ image-show,image show,Describe a specific image. image-stage,image stage,Upload data for a specific image to staging. image-tag-delete,image unset --tag <tag>,Delete the tag associated with the given image. image-tag-update,image set --tag <tag>,Update an image with the given tag. +image-tasks,,Get tasks associated with image. image-update,image set,Update an existing image. image-upload,,Upload data for a specific image. import-info,,Print import methods available from Glance. @@ -49,6 +54,7 @@ md-tag-show,,Describe a specific metadata definitions tag inside a namespace. md-tag-update,,Rename a metadata definitions tag inside a namespace. member-create,image add project,Create member for a given image. member-delete,image remove project,Delete image member. +member-get,,Show details of an image member member-list,image member list,Describe sharing permissions by image. member-update,image set --accept --reject --status,Update the status of a member for a given image. stores-delete,,Delete image from specific store. @@ -56,5 +62,6 @@ stores-info,,Print available backends from Glance. task-create,WONTFIX,Create a new task. task-list,image task list,List tasks you can access. task-show,image task show,Describe a specific task. +usage,,Get quota usage information. bash-completion,complete,Prints arguments for bash_completion. help,help,Display help about this program or one of its subcommands. diff --git a/openstackclient/compute/v2/hypervisor_stats.py b/openstackclient/compute/v2/hypervisor_stats.py index 4493e080..cb63a800 100644 --- a/openstackclient/compute/v2/hypervisor_stats.py +++ b/openstackclient/compute/v2/hypervisor_stats.py @@ -11,19 +11,49 @@ # under the License. # - """Hypervisor Stats action implementations""" from osc_lib.command import command +from osc_lib import utils from openstackclient.i18n import _ +def _get_hypervisor_stat_columns(item): + column_map = { + # NOTE(gtema): If we decide to use SDK names - empty this + 'disk_available': 'disk_available_least', + 'local_disk_free': 'free_disk_gb', + 'local_disk_size': 'local_gb', + 'local_disk_used': 'local_gb_used', + 'memory_free': 'free_ram_mb', + 'memory_size': 'memory_mb', + 'memory_used': 'memory_mb_used', + + } + hidden_columns = ['id', 'links', 'location', 'name'] + return utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden_columns) + + class ShowHypervisorStats(command.ShowOne): _description = _("Display hypervisor stats details") def take_action(self, parsed_args): - compute_client = self.app.client_manager.compute - hypervisor_stats = compute_client.hypervisors.statistics().to_dict() - - return zip(*sorted(hypervisor_stats.items())) + # The command is deprecated since it is being dropped in Nova. + self.log.warning( + _("This command is deprecated.") + ) + compute_client = self.app.client_manager.sdk_connection.compute + # We do API request directly cause this deprecated method is not and + # will not be supported by OpenStackSDK. + response = compute_client.get( + '/os-hypervisors/statistics', + microversion='2.1') + hypervisor_stats = response.json().get('hypervisor_statistics') + + display_columns, columns = _get_hypervisor_stat_columns( + hypervisor_stats) + data = utils.get_dict_properties( + hypervisor_stats, columns) + return (display_columns, data) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index f93031ec..85693e17 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -77,6 +77,10 @@ class AddressesColumn(cliff_columns.FormattableColumn): except Exception: return 'N/A' + def machine_readable(self): + return {k: [i['addr'] for i in v if 'addr' in i] + for k, v in self._value.items()} + class HostColumn(cliff_columns.FormattableColumn): """Generate a formatted string of a hostname.""" @@ -133,14 +137,61 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True): the latest details of a server after creating it. :rtype: a dict of server details """ + # Note: Some callers of this routine pass a novaclient server, and others + # pass an SDK server. Column names may be different across those cases. info = server.to_dict() if refresh: server = utils.find_resource(compute_client.servers, info['id']) info.update(server.to_dict()) + # Some commands using this routine were originally implemented with the + # nova python wrappers, and were later migrated to use the SDK. Map the + # SDK's property names to the original property names to maintain backward + # compatibility for existing users. Data is duplicated under both the old + # and new name so users can consume the data by either name. + column_map = { + 'access_ipv4': 'accessIPv4', + 'access_ipv6': 'accessIPv6', + 'admin_password': 'adminPass', + 'admin_password': 'adminPass', + 'volumes': 'os-extended-volumes:volumes_attached', + 'availability_zone': 'OS-EXT-AZ:availability_zone', + 'block_device_mapping': 'block_device_mapping_v2', + 'compute_host': 'OS-EXT-SRV-ATTR:host', + 'created_at': 'created', + 'disk_config': 'OS-DCF:diskConfig', + 'flavor_id': 'flavorRef', + 'has_config_drive': 'config_drive', + 'host_id': 'hostId', + 'fault': 'fault', + 'hostname': 'OS-EXT-SRV-ATTR:hostname', + 'hypervisor_hostname': 'OS-EXT-SRV-ATTR:hypervisor_hostname', + 'image_id': 'imageRef', + 'instance_name': 'OS-EXT-SRV-ATTR:instance_name', + 'is_locked': 'locked', + 'kernel_id': 'OS-EXT-SRV-ATTR:kernel_id', + 'launch_index': 'OS-EXT-SRV-ATTR:launch_index', + 'launched_at': 'OS-SRV-USG:launched_at', + 'power_state': 'OS-EXT-STS:power_state', + 'project_id': 'tenant_id', + 'ramdisk_id': 'OS-EXT-SRV-ATTR:ramdisk_id', + 'reservation_id': 'OS-EXT-SRV-ATTR:reservation_id', + 'root_device_name': 'OS-EXT-SRV-ATTR:root_device_name', + 'scheduler_hints': 'OS-SCH-HNT:scheduler_hints', + 'task_state': 'OS-EXT-STS:task_state', + 'terminated_at': 'OS-SRV-USG:terminated_at', + 'updated_at': 'updated', + 'user_data': 'OS-EXT-SRV-ATTR:user_data', + 'vm_state': 'OS-EXT-STS:vm_state', + } + + info.update({ + column_map[column]: data for column, data in info.items() + if column in column_map}) + # Convert the image blob to a name image_info = info.get('image', {}) - if image_info: + if image_info and any(image_info.values()): image_id = image_info.get('id', '') try: image = image_client.get_image(image_id) @@ -188,7 +239,9 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True): # NOTE(dtroyer): novaclient splits these into separate entries... # Format addresses in a useful way - info['addresses'] = format_columns.DictListColumn(server.networks) + info['addresses'] = ( + AddressesColumn(info['addresses']) if 'addresses' in info + else format_columns.DictListColumn(info.get('networks'))) # Map 'metadata' field to 'properties' info['properties'] = format_columns.DictColumn(info.pop('metadata')) @@ -290,9 +343,11 @@ class AddFixedIP(command.ShowOne): return ((), ()) kwargs = { - 'net_id': net_id, - 'fixed_ip': parsed_args.fixed_ip_address, + 'net_id': net_id } + if parsed_args.fixed_ip_address: + kwargs['fixed_ips'] = [ + {"ip_address": parsed_args.fixed_ip_address}] if parsed_args.tag: kwargs['tag'] = parsed_args.tag @@ -451,8 +506,7 @@ class AddPort(command.Command): port_id = parsed_args.port kwargs = { - 'port_id': port_id, - 'fixed_ip': None, + 'port_id': port_id } if parsed_args.tag: @@ -506,8 +560,7 @@ class AddNetwork(command.Command): net_id = parsed_args.network kwargs = { - 'net_id': net_id, - 'fixed_ip': None, + 'net_id': net_id } if parsed_args.tag: @@ -2584,9 +2637,9 @@ class ListServer(command.Lister): columns += ('Metadata',) column_headers += ('Properties',) - # convert back to tuple - column_headers = tuple(column_headers) - columns = tuple(columns) + # remove duplicates + column_headers = tuple(dict.fromkeys(column_headers)) + columns = tuple(dict.fromkeys(columns)) if parsed_args.marker is not None: # Check if both "--marker" and "--deleted" are used. @@ -4325,32 +4378,34 @@ class ShowServer(command.ShowOne): return parser def take_action(self, parsed_args): - compute_client = self.app.client_manager.compute - server = utils.find_resource( - compute_client.servers, parsed_args.server) + compute_client = self.app.client_manager.sdk_connection.compute + + # Find by name or ID, then get the full details of the server + server = compute_client.find_server( + parsed_args.server, ignore_missing=False) + server = compute_client.get_server(server) if parsed_args.diagnostics: - (resp, data) = server.diagnostics() - if not resp.status_code == 200: - self.app.stderr.write(_( - "Error retrieving diagnostics data\n" - )) - return ({}, {}) + data = compute_client.get_server_diagnostics(server) return zip(*sorted(data.items())) topology = None if parsed_args.topology: - if compute_client.api_version < api_versions.APIVersion('2.78'): + if not sdk_utils.supports_microversion(compute_client, '2.78'): msg = _( '--os-compute-api-version 2.78 or greater is required to ' 'support the --topology option' ) raise exceptions.CommandError(msg) - topology = server.topology() + topology = server.fetch_topology(compute_client) data = _prep_server_detail( - compute_client, self.app.client_manager.image, server, + # TODO(dannosliwcd): Replace these clients with SDK clients after + # all callers of _prep_server_detail() are using the SDK. + self.app.client_manager.compute, + self.app.client_manager.image, + server, refresh=False) if topology: diff --git a/openstackclient/compute/v2/server_migration.py b/openstackclient/compute/v2/server_migration.py index 919b67bd..016d15d7 100644 --- a/openstackclient/compute/v2/server_migration.py +++ b/openstackclient/compute/v2/server_migration.py @@ -15,6 +15,7 @@ import uuid from novaclient import api_versions +from openstack import utils as sdk_utils from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -130,22 +131,22 @@ class ListMigration(command.Lister): # the same as the column header names. columns = [ 'source_node', 'dest_node', 'source_compute', 'dest_compute', - 'dest_host', 'status', 'instance_uuid', 'old_instance_type_id', - 'new_instance_type_id', 'created_at', 'updated_at', + 'dest_host', 'status', 'server_id', 'old_flavor_id', + 'new_flavor_id', 'created_at', 'updated_at', ] # Insert migrations UUID after ID - if compute_client.api_version >= api_versions.APIVersion("2.59"): + if sdk_utils.supports_microversion(compute_client, "2.59"): column_headers.insert(0, "UUID") columns.insert(0, "uuid") - if compute_client.api_version >= api_versions.APIVersion("2.23"): + if sdk_utils.supports_microversion(compute_client, "2.23"): column_headers.insert(0, "Id") columns.insert(0, "id") column_headers.insert(len(column_headers) - 2, "Type") columns.insert(len(columns) - 2, "migration_type") - if compute_client.api_version >= api_versions.APIVersion("2.80"): + if sdk_utils.supports_microversion(compute_client, "2.80"): if parsed_args.project: column_headers.insert(len(column_headers) - 2, "Project") columns.insert(len(columns) - 2, "project_id") @@ -159,19 +160,23 @@ class ListMigration(command.Lister): ) def take_action(self, parsed_args): - compute_client = self.app.client_manager.compute + compute_client = self.app.client_manager.sdk_connection.compute identity_client = self.app.client_manager.identity - search_opts = { - 'host': parsed_args.host, - 'status': parsed_args.status, - } + search_opts = {} + + if parsed_args.host is not None: + search_opts['host'] = parsed_args.host + + if parsed_args.status is not None: + search_opts['status'] = parsed_args.status if parsed_args.server: - search_opts['instance_uuid'] = utils.find_resource( - compute_client.servers, - parsed_args.server, - ).id + server = compute_client.find_server(parsed_args.server) + if server is None: + msg = _('Unable to find server: %s') % parsed_args.server + raise exceptions.CommandError(msg) + search_opts['instance_uuid'] = server.id if parsed_args.type: migration_type = parsed_args.type @@ -181,7 +186,7 @@ class ListMigration(command.Lister): search_opts['migration_type'] = migration_type if parsed_args.marker: - if compute_client.api_version < api_versions.APIVersion('2.59'): + if not sdk_utils.supports_microversion(compute_client, "2.59"): msg = _( '--os-compute-api-version 2.59 or greater is required to ' 'support the --marker option' @@ -190,16 +195,17 @@ class ListMigration(command.Lister): search_opts['marker'] = parsed_args.marker if parsed_args.limit: - if compute_client.api_version < api_versions.APIVersion('2.59'): + if not sdk_utils.supports_microversion(compute_client, "2.59"): msg = _( '--os-compute-api-version 2.59 or greater is required to ' 'support the --limit option' ) raise exceptions.CommandError(msg) search_opts['limit'] = parsed_args.limit + search_opts['paginated'] = False if parsed_args.changes_since: - if compute_client.api_version < api_versions.APIVersion('2.59'): + if not sdk_utils.supports_microversion(compute_client, "2.59"): msg = _( '--os-compute-api-version 2.59 or greater is required to ' 'support the --changes-since option' @@ -208,7 +214,7 @@ class ListMigration(command.Lister): search_opts['changes_since'] = parsed_args.changes_since if parsed_args.changes_before: - if compute_client.api_version < api_versions.APIVersion('2.66'): + if not sdk_utils.supports_microversion(compute_client, "2.66"): msg = _( '--os-compute-api-version 2.66 or greater is required to ' 'support the --changes-before option' @@ -217,7 +223,7 @@ class ListMigration(command.Lister): search_opts['changes_before'] = parsed_args.changes_before if parsed_args.project: - if compute_client.api_version < api_versions.APIVersion('2.80'): + if not sdk_utils.supports_microversion(compute_client, "2.80"): msg = _( '--os-compute-api-version 2.80 or greater is required to ' 'support the --project option' @@ -231,7 +237,7 @@ class ListMigration(command.Lister): ).id if parsed_args.user: - if compute_client.api_version < api_versions.APIVersion('2.80'): + if not sdk_utils.supports_microversion(compute_client, "2.80"): msg = _( '--os-compute-api-version 2.80 or greater is required to ' 'support the --user option' @@ -244,7 +250,7 @@ class ListMigration(command.Lister): parsed_args.user_domain, ).id - migrations = compute_client.migrations.list(**search_opts) + migrations = list(compute_client.migrations(**search_opts)) return self.print_migrations(parsed_args, compute_client, migrations) diff --git a/openstackclient/image/v2/metadef_namespaces.py b/openstackclient/image/v2/metadef_namespaces.py index 158fd94e..f09f2002 100644 --- a/openstackclient/image/v2/metadef_namespaces.py +++ b/openstackclient/image/v2/metadef_namespaces.py @@ -15,8 +15,11 @@ """Image V2 Action Implementations""" +import logging + from osc_lib.cli import format_columns from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils from openstackclient.i18n import _ @@ -25,6 +28,149 @@ _formatters = { 'tags': format_columns.ListColumn, } +LOG = logging.getLogger(__name__) + + +def _format_namespace(namespace): + info = {} + + fields_to_show = [ + 'created_at', + 'description', + 'display_name', + 'namespace', + 'owner', + 'protected', + 'schema', + 'visibility', + ] + + namespace = namespace.to_dict(ignore_none=True, original_names=True) + + # split out the usual key and the properties which are top-level + for key in namespace: + if key in fields_to_show: + info[key] = namespace.get(key) + elif key == "resource_type_associations": + info[key] = [resource_type['name'] + for resource_type in namespace.get(key)] + elif key == 'properties': + info['properties'] = list(namespace.get(key).keys()) + + return info + + +class CreateMetadefNameSpace(command.ShowOne): + _description = _("Create a metadef namespace") + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + "namespace", + metavar="<namespace>", + help=_("New metadef namespace name"), + ) + parser.add_argument( + "--display-name", + metavar="<display_name>", + help=_("A user-friendly name for the namespace."), + ) + parser.add_argument( + "--description", + metavar="<description>", + help=_("A description of the namespace"), + ) + visibility_group = parser.add_mutually_exclusive_group() + visibility_group.add_argument( + "--public", + action="store_const", + const="public", + dest="visibility", + help=_("Set namespace visibility 'public'"), + ) + visibility_group.add_argument( + "--private", + action="store_const", + const="private", + dest="visibility", + help=_("Set namespace visibility 'private'"), + ) + protected_group = parser.add_mutually_exclusive_group() + protected_group.add_argument( + "--protected", + action="store_const", + const=True, + dest="is_protected", + help=_("Prevent metadef namespace from being deleted"), + ) + protected_group.add_argument( + "--unprotected", + action="store_const", + const=False, + dest="is_protected", + help=_("Allow metadef namespace to be deleted (default)"), + ) + return parser + + def take_action(self, parsed_args): + image_client = self.app.client_manager.image + filter_keys = [ + 'namespace', + 'display_name', + 'description' + ] + kwargs = {} + + for key in filter_keys: + argument = getattr(parsed_args, key, None) + if argument is not None: + kwargs[key] = argument + + if parsed_args.is_protected is not None: + kwargs['protected'] = parsed_args.is_protected + + if parsed_args.visibility is not None: + kwargs['visibility'] = parsed_args.visibility + + data = image_client.create_metadef_namespace(**kwargs) + + return zip(*sorted(data.items())) + + +class DeleteMetadefNameSpace(command.Command): + _description = _("Delete metadef namespace") + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + "namespace_name", + metavar="<namespace_name>", + nargs="+", + help=_("An identifier (a name) for the namespace"), + ) + return parser + + def take_action(self, parsed_args): + image_client = self.app.client_manager.image + + result = 0 + for i in parsed_args.namespace_name: + try: + namespace = image_client.get_metadef_namespace(i) + image_client.delete_metadef_namespace(namespace.id) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete namespace with name or " + "ID '%(namespace)s': %(e)s"), + {'namespace': i, 'e': e} + ) + + if result > 0: + total = len(parsed_args.namespace_name) + msg = (_("%(result)s of %(total)s namespace failed " + "to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) + class ListMetadefNameSpaces(command.Lister): _description = _("List metadef namespaces") @@ -63,3 +209,104 @@ class ListMetadefNameSpaces(command.Lister): formatters=_formatters, ) for s in data) ) + + +class SetMetadefNameSpace(command.Command): + _description = _("Set metadef namespace properties") + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + "namespace", + metavar="<namespace>", + help=_("Namespace (name) for the namespace"), + ) + parser.add_argument( + "--display-name", + metavar="<display_name>", + help=_("Set a user-friendly name for the namespace."), + ) + parser.add_argument( + "--description", + metavar="<description>", + help=_("Set the description of the namespace"), + ) + visibility_group = parser.add_mutually_exclusive_group() + visibility_group.add_argument( + "--public", + action="store_const", + const="public", + dest="visibility", + help=_("Set namespace visibility 'public'"), + ) + visibility_group.add_argument( + "--private", + action="store_const", + const="private", + dest="visibility", + help=_("Set namespace visibility 'private'"), + ) + protected_group = parser.add_mutually_exclusive_group() + protected_group.add_argument( + "--protected", + action="store_const", + const=True, + dest="is_protected", + help=_("Prevent metadef namespace from being deleted"), + ) + protected_group.add_argument( + "--unprotected", + action="store_const", + const=False, + dest="is_protected", + help=_("Allow metadef namespace to be deleted (default)"), + ) + return parser + + def take_action(self, parsed_args): + image_client = self.app.client_manager.image + + namespace = parsed_args.namespace + + filter_keys = [ + 'namespace', + 'display_name', + 'description' + ] + kwargs = {} + + for key in filter_keys: + argument = getattr(parsed_args, key, None) + if argument is not None: + kwargs[key] = argument + + if parsed_args.is_protected is not None: + kwargs['protected'] = parsed_args.is_protected + + if parsed_args.visibility is not None: + kwargs['visibility'] = parsed_args.visibility + + image_client.update_metadef_namespace(namespace, **kwargs) + + +class ShowMetadefNameSpace(command.ShowOne): + _description = _("Show a metadef namespace") + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + "namespace_name", + metavar="<namespace_name>", + help=_("Namespace (name) for the namespace"), + ) + return parser + + def take_action(self, parsed_args): + image_client = self.app.client_manager.image + + namespace_name = parsed_args.namespace_name + + data = image_client.get_metadef_namespace(namespace_name) + info = _format_namespace(data) + + return zip(*sorted(info.items())) diff --git a/openstackclient/network/v2/network_agent.py b/openstackclient/network/v2/network_agent.py index d963b3bf..f67f67bd 100644 --- a/openstackclient/network/v2/network_agent.py +++ b/openstackclient/network/v2/network_agent.py @@ -168,11 +168,11 @@ class ListNetworkAgent(command.Lister): metavar='<agent-type>', choices=["bgp", "dhcp", "open-vswitch", "linux-bridge", "ofa", "l3", "loadbalancer", "metering", "metadata", "macvtap", - "nic"], + "nic", "baremetal"], help=_("List only agents with the specified agent type. " "The supported agent types are: bgp, dhcp, open-vswitch, " "linux-bridge, ofa, l3, loadbalancer, metering, " - "metadata, macvtap, nic.") + "metadata, macvtap, nic, baremetal.") ) parser.add_argument( '--host', @@ -231,7 +231,8 @@ class ListNetworkAgent(command.Lister): 'metering': 'Metering agent', 'metadata': 'Metadata agent', 'macvtap': 'Macvtap agent', - 'nic': 'NIC Switch agent' + 'nic': 'NIC Switch agent', + 'baremetal': 'Baremetal Node' } filters = {} diff --git a/openstackclient/tests/functional/compute/v2/test_hypervisor.py b/openstackclient/tests/functional/compute/v2/test_hypervisor.py new file mode 100644 index 00000000..9bc23280 --- /dev/null +++ b/openstackclient/tests/functional/compute/v2/test_hypervisor.py @@ -0,0 +1,52 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +from openstackclient.tests.functional import base + + +class HypervisorTests(base.TestCase): + """Functional tests for hypervisor.""" + + def test_hypervisor_list(self): + """Test create defaults, list filters, delete""" + # Test list + cmd_output = json.loads(self.openstack( + "hypervisor list -f json --os-compute-api-version 2.1" + )) + ids1 = [x["ID"] for x in cmd_output] + self.assertIsNotNone(cmd_output) + + cmd_output = json.loads(self.openstack( + "hypervisor list -f json" + )) + ids2 = [x["ID"] for x in cmd_output] + self.assertIsNotNone(cmd_output) + + # Show test - old microversion + for i in ids1: + cmd_output = json.loads(self.openstack( + "hypervisor show %s -f json " + " --os-compute-api-version 2.1" + % (i) + )) + self.assertIsNotNone(cmd_output) + # When we list hypervisors with older MV we get ids as integers. We + # need to verify that show finds resources independently + # Show test - latest microversion + for i in ids2: + cmd_output = json.loads(self.openstack( + "hypervisor show %s -f json" + % (i) + )) + self.assertIsNotNone(cmd_output) diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py index 830b3543..37183a79 100644 --- a/openstackclient/tests/functional/compute/v2/test_server.py +++ b/openstackclient/tests/functional/compute/v2/test_server.py @@ -11,6 +11,7 @@ # under the License. import itertools +import json import time import uuid @@ -288,6 +289,33 @@ class ServerTests(common.ComputeTestCase): ) self.assertOutput("", raw_output) + def test_server_show(self): + """Test server show""" + cmd_output = self.server_create() + name = cmd_output['name'] + + # Simple show + cmd_output = json.loads(self.openstack( + f'server show -f json {name}' + )) + self.assertEqual( + name, + cmd_output["name"], + ) + + # Show diagnostics + cmd_output = json.loads(self.openstack( + f'server show -f json {name} --diagnostics' + )) + self.assertIn('driver', cmd_output) + + # Show topology + cmd_output = json.loads(self.openstack( + f'server show -f json {name} --topology ' + f'--os-compute-api-version 2.78' + )) + self.assertIn('topology', cmd_output) + def test_server_actions(self): """Test server action pairs @@ -1250,6 +1278,62 @@ class ServerTests(common.ComputeTestCase): addresses = cmd_output['addresses']['private'] self.assertNotIn(ip_address, addresses) + def test_server_add_fixed_ip(self): + name = uuid.uuid4().hex + cmd_output = self.openstack( + 'server create ' + + '--network private ' + + '--flavor ' + self.flavor_name + ' ' + + '--image ' + self.image_name + ' ' + + '--wait ' + + name, + parse_output=True, + ) + + self.assertIsNotNone(cmd_output['id']) + self.assertEqual(name, cmd_output['name']) + self.addCleanup(self.openstack, 'server delete --wait ' + name) + + # create port, record its ip address to use in later call, + # then delete - this is to figure out what should be a free ip + # in the subnet + port_name = uuid.uuid4().hex + + cmd_output = self.openstack( + 'port list', + parse_output=True, + ) + self.assertNotIn(port_name, cmd_output) + + cmd_output = self.openstack( + 'port create ' + + '--network private ' + port_name, + parse_output=True, + ) + self.assertIsNotNone(cmd_output['id']) + ip_address = cmd_output['fixed_ips'][0]['ip_address'] + self.openstack('port delete ' + port_name) + + # add fixed ip to server, assert the ip address appears + self.openstack('server add fixed ip --fixed-ip-address ' + ip_address + + ' ' + name + ' private') + + wait_time = 0 + while wait_time < 60: + cmd_output = self.openstack( + 'server show ' + name, + parse_output=True, + ) + if ip_address not in cmd_output['addresses']['private']: + # Hang out for a bit and try again + print('retrying add fixed ip check') + wait_time += 10 + time.sleep(10) + else: + break + addresses = cmd_output['addresses']['private'] + self.assertIn(ip_address, addresses) + def test_server_add_remove_volume(self): volume_wait_for = volume_common.BaseVolumeTests.wait_for_status @@ -1331,3 +1415,9 @@ class ServerTests(common.ComputeTestCase): raw_output = self.openstack('server volume list ' + server_name) self.assertEqual('\n', raw_output) + + def test_server_migration_list(self): + # Verify that the command does not raise an exception when we list + # migrations, including when we specify a query. + self.openstack('server migration list') + self.openstack('server migration list --limit 1') diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index 4a27a941..b2702128 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -341,67 +341,6 @@ class FakeExtension(object): return extension -class FakeHypervisorStats(object): - """Fake one or more hypervisor stats.""" - - @staticmethod - def create_one_hypervisor_stats(attrs=None): - """Create a fake hypervisor stats. - - :param dict attrs: - A dictionary with all attributes - :return: - A FakeResource object, with count, current_workload, and so on - """ - attrs = attrs or {} - - # Set default attributes. - stats_info = { - 'count': 2, - 'current_workload': 0, - 'disk_available_least': 50, - 'free_disk_gb': 100, - 'free_ram_mb': 23000, - 'local_gb': 100, - 'local_gb_used': 0, - 'memory_mb': 23800, - 'memory_mb_used': 1400, - 'running_vms': 3, - 'vcpus': 8, - 'vcpus_used': 3, - } - - # Overwrite default attributes. - stats_info.update(attrs) - - # Set default method. - hypervisor_stats_method = {'to_dict': stats_info} - - hypervisor_stats = fakes.FakeResource( - info=copy.deepcopy(stats_info), - methods=copy.deepcopy(hypervisor_stats_method), - loaded=True) - return hypervisor_stats - - @staticmethod - def create_hypervisors_stats(attrs=None, count=2): - """Create multiple fake hypervisors stats. - - :param dict attrs: - A dictionary with all attributes - :param int count: - The number of hypervisors to fake - :return: - A list of FakeResource objects faking the hypervisors - """ - hypervisors = [] - for i in range(0, count): - hypervisors.append( - FakeHypervisorStats.create_one_hypervisor_stats(attrs)) - - return hypervisors - - class FakeSecurityGroup(object): """Fake one or more security groups.""" diff --git a/openstackclient/tests/unit/compute/v2/test_hypervisor_stats.py b/openstackclient/tests/unit/compute/v2/test_hypervisor_stats.py index 40086f9b..7bc7468a 100644 --- a/openstackclient/tests/unit/compute/v2/test_hypervisor_stats.py +++ b/openstackclient/tests/unit/compute/v2/test_hypervisor_stats.py @@ -12,9 +12,11 @@ # License for the specific language governing permissions and limitations # under the License. # +from unittest import mock from openstackclient.compute.v2 import hypervisor_stats from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes +from openstackclient.tests.unit import fakes class TestHypervisorStats(compute_fakes.TestComputev2): @@ -23,20 +25,55 @@ class TestHypervisorStats(compute_fakes.TestComputev2): super(TestHypervisorStats, self).setUp() # Get a shortcut to the compute client hypervisors mock - self.hypervisors_mock = self.app.client_manager.compute.hypervisors - self.hypervisors_mock.reset_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.get = mock.Mock() + + +# Not in fakes.py because hypervisor stats has been deprecated + +def create_one_hypervisor_stats(attrs=None): + """Create a fake hypervisor stats. + + :param dict attrs: + A dictionary with all attributes + :return: + A dictionary that contains hypervisor stats information keys + """ + attrs = attrs or {} + + # Set default attributes. + stats_info = { + 'count': 2, + 'current_workload': 0, + 'disk_available_least': 50, + 'free_disk_gb': 100, + 'free_ram_mb': 23000, + 'local_gb': 100, + 'local_gb_used': 0, + 'memory_mb': 23800, + 'memory_mb_used': 1400, + 'running_vms': 3, + 'vcpus': 8, + 'vcpus_used': 3, + } + + # Overwrite default attributes. + stats_info.update(attrs) + + return stats_info class TestHypervisorStatsShow(TestHypervisorStats): + _stats = create_one_hypervisor_stats() + def setUp(self): super(TestHypervisorStatsShow, self).setUp() - self.hypervisor_stats = \ - compute_fakes.FakeHypervisorStats.create_one_hypervisor_stats() - - self.hypervisors_mock.statistics.return_value =\ - self.hypervisor_stats + self.sdk_client.get.return_value = fakes.FakeResponse( + data={'hypervisor_statistics': self._stats}) self.cmd = hypervisor_stats.ShowHypervisorStats(self.app, None) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index ea8ded5c..00f9b3a7 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -423,8 +423,7 @@ class TestServerAddFixedIP(TestServer): self.assertEqual(expected_data, tuple(data)) self.sdk_client.create_server_interface.assert_called_once_with( servers[0].id, - net_id=network['id'], - fixed_ip=None + net_id=network['id'] ) @mock.patch.object(sdk_utils, 'supports_microversion') @@ -479,7 +478,7 @@ class TestServerAddFixedIP(TestServer): self.sdk_client.create_server_interface.assert_called_once_with( servers[0].id, net_id=network['id'], - fixed_ip='5.6.7.8' + fixed_ips=[{'ip_address': '5.6.7.8'}] ) @mock.patch.object(sdk_utils, 'supports_microversion') @@ -536,7 +535,7 @@ class TestServerAddFixedIP(TestServer): self.sdk_client.create_server_interface.assert_called_once_with( servers[0].id, net_id=network['id'], - fixed_ip='5.6.7.8', + fixed_ips=[{'ip_address': '5.6.7.8'}], tag='tag1', ) @@ -847,7 +846,7 @@ class TestServerAddPort(TestServer): result = self.cmd.take_action(parsed_args) self.sdk_client.create_server_interface.assert_called_once_with( - servers[0], port_id=port_id, fixed_ip=None) + servers[0], port_id=port_id) self.assertIsNone(result) def test_server_add_port(self): @@ -885,7 +884,6 @@ class TestServerAddPort(TestServer): self.sdk_client.create_server_interface.assert_called_once_with( servers[0], port_id='fake-port', - fixed_ip=None, tag='tag1') @mock.patch.object(sdk_utils, 'supports_microversion', return_value=False) @@ -1288,7 +1286,7 @@ class TestServerAddNetwork(TestServer): result = self.cmd.take_action(parsed_args) self.sdk_client.create_server_interface.assert_called_once_with( - servers[0], net_id=net_id, fixed_ip=None) + servers[0], net_id=net_id) self.assertIsNone(result) def test_server_add_network(self): @@ -1327,7 +1325,6 @@ class TestServerAddNetwork(TestServer): self.sdk_client.create_server_interface.assert_called_once_with( servers[0], net_id='fake-network', - fixed_ip=None, tag='tag1' ) @@ -4721,6 +4718,7 @@ class TestServerList(_TestServerList): self.assertIn('Availability Zone', columns) self.assertIn('Host', columns) self.assertIn('Properties', columns) + self.assertCountEqual(columns, set(columns)) def test_server_list_no_name_lookup_option(self): self.data = tuple( @@ -7948,20 +7946,15 @@ class TestServerShow(TestServer): 'tenant_id': 'tenant-id-xxx', 'networks': {'public': ['10.20.30.40', '2001:db8::f']}, } - # Fake the server.diagnostics() method. The return value contains http - # response and data. The data is a dict. Sincce this method itself is - # faked, we don't need to fake everything of the return value exactly. - resp = mock.Mock() - resp.status_code = 200 + self.sdk_client.get_server_diagnostics.return_value = {'test': 'test'} server_method = { - 'diagnostics': (resp, {'test': 'test'}), - 'topology': self.topology, + 'fetch_topology': self.topology, } self.server = compute_fakes.FakeServer.create_one_server( attrs=server_info, methods=server_method) # This is the return value for utils.find_resource() - self.servers_mock.get.return_value = self.server + self.sdk_client.get_server.return_value = self.server self.get_image_mock.return_value = self.image self.flavors_mock.get.return_value = self.flavor @@ -8062,8 +8055,7 @@ class TestServerShow(TestServer): self.assertEqual(('test',), data) def test_show_topology(self): - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.78') + self._set_mock_microversion('2.78') arglist = [ '--topology', @@ -8085,8 +8077,7 @@ class TestServerShow(TestServer): self.assertCountEqual(self.data, data) def test_show_topology_pre_v278(self): - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.77') + self._set_mock_microversion('2.77') arglist = [ '--topology', diff --git a/openstackclient/tests/unit/compute/v2/test_server_migration.py b/openstackclient/tests/unit/compute/v2/test_server_migration.py index c4cbac47..93c1865a 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_migration.py +++ b/openstackclient/tests/unit/compute/v2/test_server_migration.py @@ -13,6 +13,7 @@ from unittest import mock from novaclient import api_versions +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils as common_utils @@ -35,10 +36,6 @@ class TestServerMigration(compute_fakes.TestComputev2): self.app.client_manager.compute.server_migrations self.server_migrations_mock.reset_mock() - # Get a shortcut to the compute client MigrationManager mock - self.migrations_mock = self.app.client_manager.compute.migrations - self.migrations_mock.reset_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 @@ -53,22 +50,21 @@ class TestListMigration(TestServerMigration): 'Old Flavor', 'New Flavor', 'Created At', 'Updated At' ] - # These are the fields that come back in the response from the REST API. MIGRATION_FIELDS = [ 'source_node', 'dest_node', 'source_compute', 'dest_compute', - 'dest_host', 'status', 'instance_uuid', 'old_instance_type_id', - 'new_instance_type_id', 'created_at', 'updated_at' + 'dest_host', 'status', 'server_id', 'old_flavor_id', + 'new_flavor_id', 'created_at', 'updated_at' ] def setUp(self): super().setUp() self.server = compute_fakes.FakeServer.create_one_server() - self.servers_mock.get.return_value = self.server + self.sdk_client.find_server.return_value = self.server self.migrations = compute_fakes.FakeMigration.create_migrations( count=3) - self.migrations_mock.list.return_value = self.migrations + self.sdk_client.migrations.return_value = self.migrations self.data = (common_utils.get_item_properties( s, self.MIGRATION_FIELDS) for s in self.migrations) @@ -76,6 +72,20 @@ class TestListMigration(TestServerMigration): # Get the command object to test self.cmd = server_migration.ListMigration(self.app, None) + patcher = mock.patch.object( + sdk_utils, 'supports_microversion', return_value=True) + self.addCleanup(patcher.stop) + self.supports_microversion_mock = patcher.start() + self._set_mock_microversion( + self.app.client_manager.compute.api_version.get_string()) + + def _set_mock_microversion(self, mock_v): + """Set a specific microversion for the mock supports_microversion().""" + self.supports_microversion_mock.reset_mock(return_value=True) + self.supports_microversion_mock.side_effect = ( + lambda _, v: + api_versions.APIVersion(v) <= api_versions.APIVersion(mock_v)) + def test_server_migration_list_no_options(self): arglist = [] verifylist = [] @@ -84,12 +94,9 @@ class TestListMigration(TestServerMigration): columns, data = self.cmd.take_action(parsed_args) # Set expected values - kwargs = { - 'status': None, - 'host': None, - } + kwargs = {} - self.migrations_mock.list.assert_called_with(**kwargs) + self.sdk_client.migrations.assert_called_with(**kwargs) self.assertEqual(self.MIGRATION_COLUMNS, columns) self.assertEqual(tuple(self.data), tuple(data)) @@ -117,8 +124,8 @@ class TestListMigration(TestServerMigration): 'migration_type': 'migration', } - self.servers_mock.get.assert_called_with('server1') - self.migrations_mock.list.assert_called_with(**kwargs) + self.sdk_client.find_server.assert_called_with('server1') + self.sdk_client.migrations.assert_called_with(**kwargs) self.assertEqual(self.MIGRATION_COLUMNS, columns) self.assertEqual(tuple(self.data), tuple(data)) @@ -133,18 +140,17 @@ class TestListMigrationV223(TestListMigration): 'Type', 'Created At', 'Updated At' ] - # These are the fields that come back in the response from the REST API. + # These are the Migration object fields. MIGRATION_FIELDS = [ 'id', 'source_node', 'dest_node', 'source_compute', 'dest_compute', - 'dest_host', 'status', 'instance_uuid', 'old_instance_type_id', - 'new_instance_type_id', 'migration_type', 'created_at', 'updated_at' + 'dest_host', 'status', 'server_id', 'old_flavor_id', + 'new_flavor_id', 'migration_type', 'created_at', 'updated_at' ] def setUp(self): super().setUp() - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.23') + self._set_mock_microversion('2.23') def test_server_migration_list(self): arglist = [ @@ -159,10 +165,9 @@ class TestListMigrationV223(TestListMigration): # Set expected values kwargs = { 'status': 'migrating', - 'host': None, } - self.migrations_mock.list.assert_called_with(**kwargs) + self.sdk_client.migrations.assert_called_with(**kwargs) self.assertEqual(self.MIGRATION_COLUMNS, columns) self.assertEqual(tuple(self.data), tuple(data)) @@ -177,19 +182,18 @@ class TestListMigrationV259(TestListMigration): 'Old Flavor', 'New Flavor', 'Type', 'Created At', 'Updated At' ] - # These are the fields that come back in the response from the REST API. + # These are the Migration object fields. MIGRATION_FIELDS = [ 'id', 'uuid', 'source_node', 'dest_node', 'source_compute', - 'dest_compute', 'dest_host', 'status', 'instance_uuid', - 'old_instance_type_id', 'new_instance_type_id', 'migration_type', + 'dest_compute', 'dest_host', 'status', 'server_id', + 'old_flavor_id', 'new_flavor_id', 'migration_type', 'created_at', 'updated_at' ] def setUp(self): super().setUp() - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.59') + self._set_mock_microversion('2.59') def test_server_migration_list(self): arglist = [ @@ -211,19 +215,18 @@ class TestListMigrationV259(TestListMigration): kwargs = { 'status': 'migrating', 'limit': 1, + 'paginated': False, 'marker': 'test_kp', - 'host': None, 'changes_since': '2019-08-09T08:03:25Z', } - self.migrations_mock.list.assert_called_with(**kwargs) + self.sdk_client.migrations.assert_called_with(**kwargs) self.assertEqual(self.MIGRATION_COLUMNS, columns) self.assertEqual(tuple(self.data), tuple(data)) def test_server_migration_list_with_limit_pre_v259(self): - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.58') + self._set_mock_microversion('2.58') arglist = [ '--status', 'migrating', '--limit', '1' @@ -242,8 +245,7 @@ class TestListMigrationV259(TestListMigration): str(ex)) def test_server_migration_list_with_marker_pre_v259(self): - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.58') + self._set_mock_microversion('2.58') arglist = [ '--status', 'migrating', '--marker', 'test_kp' @@ -262,8 +264,7 @@ class TestListMigrationV259(TestListMigration): str(ex)) def test_server_migration_list_with_changes_since_pre_v259(self): - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.58') + self._set_mock_microversion('2.58') arglist = [ '--status', 'migrating', '--changes-since', '2019-08-09T08:03:25Z' @@ -291,19 +292,18 @@ class TestListMigrationV266(TestListMigration): 'Old Flavor', 'New Flavor', 'Type', 'Created At', 'Updated At' ] - # These are the fields that come back in the response from the REST API. + # These are the Migration object fields. MIGRATION_FIELDS = [ 'id', 'uuid', 'source_node', 'dest_node', 'source_compute', - 'dest_compute', 'dest_host', 'status', 'instance_uuid', - 'old_instance_type_id', 'new_instance_type_id', 'migration_type', + 'dest_compute', 'dest_host', 'status', 'server_id', + 'old_flavor_id', 'new_flavor_id', 'migration_type', 'created_at', 'updated_at' ] def setUp(self): super().setUp() - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.66') + self._set_mock_microversion('2.66') def test_server_migration_list_with_changes_before(self): arglist = [ @@ -327,20 +327,19 @@ class TestListMigrationV266(TestListMigration): kwargs = { 'status': 'migrating', 'limit': 1, + 'paginated': False, 'marker': 'test_kp', - 'host': None, 'changes_since': '2019-08-07T08:03:25Z', 'changes_before': '2019-08-09T08:03:25Z', } - self.migrations_mock.list.assert_called_with(**kwargs) + self.sdk_client.migrations.assert_called_with(**kwargs) self.assertEqual(self.MIGRATION_COLUMNS, columns) self.assertEqual(tuple(self.data), tuple(data)) def test_server_migration_list_with_changes_before_pre_v266(self): - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.65') + self._set_mock_microversion('2.65') arglist = [ '--status', 'migrating', '--changes-before', '2019-08-09T08:03:25Z' @@ -368,11 +367,11 @@ class TestListMigrationV280(TestListMigration): 'Old Flavor', 'New Flavor', 'Type', 'Created At', 'Updated At' ] - # These are the fields that come back in the response from the REST API. + # These are the Migration object fields. MIGRATION_FIELDS = [ 'id', 'uuid', 'source_node', 'dest_node', 'source_compute', - 'dest_compute', 'dest_host', 'status', 'instance_uuid', - 'old_instance_type_id', 'new_instance_type_id', 'migration_type', + 'dest_compute', 'dest_host', 'status', 'server_id', + 'old_flavor_id', 'new_flavor_id', 'migration_type', 'created_at', 'updated_at' ] @@ -391,8 +390,7 @@ class TestListMigrationV280(TestListMigration): self.projects_mock.get.return_value = self.project self.users_mock.get.return_value = self.user - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.80') + self._set_mock_microversion('2.80') def test_server_migration_list_with_project(self): arglist = [ @@ -418,14 +416,14 @@ class TestListMigrationV280(TestListMigration): kwargs = { 'status': 'migrating', 'limit': 1, + 'paginated': False, 'marker': 'test_kp', - 'host': None, 'project_id': self.project.id, 'changes_since': '2019-08-07T08:03:25Z', 'changes_before': "2019-08-09T08:03:25Z", } - self.migrations_mock.list.assert_called_with(**kwargs) + self.sdk_client.migrations.assert_called_with(**kwargs) self.MIGRATION_COLUMNS.insert( len(self.MIGRATION_COLUMNS) - 2, "Project") @@ -439,8 +437,7 @@ class TestListMigrationV280(TestListMigration): self.MIGRATION_FIELDS.remove('project_id') def test_get_migrations_with_project_pre_v280(self): - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.79') + self._set_mock_microversion('2.79') arglist = [ '--status', 'migrating', '--changes-before', '2019-08-09T08:03:25Z', @@ -478,20 +475,21 @@ class TestListMigrationV280(TestListMigration): ('user', self.user.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) # Set expected values kwargs = { 'status': 'migrating', 'limit': 1, + 'paginated': False, 'marker': 'test_kp', - 'host': None, 'user_id': self.user.id, 'changes_since': '2019-08-07T08:03:25Z', 'changes_before': "2019-08-09T08:03:25Z", } - self.migrations_mock.list.assert_called_with(**kwargs) + self.sdk_client.migrations.assert_called_with(**kwargs) self.MIGRATION_COLUMNS.insert( len(self.MIGRATION_COLUMNS) - 2, "User") @@ -505,8 +503,7 @@ class TestListMigrationV280(TestListMigration): self.MIGRATION_FIELDS.remove('user_id') def test_get_migrations_with_user_pre_v280(self): - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.79') + self._set_mock_microversion('2.79') arglist = [ '--status', 'migrating', '--changes-before', '2019-08-09T08:03:25Z', @@ -550,14 +547,14 @@ class TestListMigrationV280(TestListMigration): kwargs = { 'status': 'migrating', 'limit': 1, - 'host': None, + 'paginated': False, 'project_id': self.project.id, 'user_id': self.user.id, 'changes_since': '2019-08-07T08:03:25Z', 'changes_before': "2019-08-09T08:03:25Z", } - self.migrations_mock.list.assert_called_with(**kwargs) + self.sdk_client.migrations.assert_called_with(**kwargs) self.MIGRATION_COLUMNS.insert( len(self.MIGRATION_COLUMNS) - 2, "Project") @@ -576,8 +573,7 @@ class TestListMigrationV280(TestListMigration): self.MIGRATION_FIELDS.remove('user_id') def test_get_migrations_with_project_and_user_pre_v280(self): - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.79') + self._set_mock_microversion('2.79') arglist = [ '--status', 'migrating', '--changes-before', '2019-08-09T08:03:25Z', diff --git a/openstackclient/tests/unit/image/v2/fakes.py b/openstackclient/tests/unit/image/v2/fakes.py index ded9ff31..8ddd9a09 100644 --- a/openstackclient/tests/unit/image/v2/fakes.py +++ b/openstackclient/tests/unit/image/v2/fakes.py @@ -239,7 +239,11 @@ def create_tasks(attrs=None, count=2): class FakeMetadefNamespaceClient: def __init__(self, **kwargs): + self.create_metadef_namespace = mock.Mock() + self.delete_metadef_namespace = mock.Mock() self.metadef_namespaces = mock.Mock() + self.get_metadef_namespace = mock.Mock() + self.update_metadef_namespace = mock.Mock() self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] @@ -277,10 +281,11 @@ def create_one_metadef_namespace(attrs=None): 'display_name': 'Flavor Quota', 'namespace': 'OS::Compute::Quota', 'owner': 'admin', - 'resource_type_associations': ['OS::Nova::Flavor'], + # 'resource_type_associations': ['OS::Nova::Flavor'], + # The part that receives the list type factor is not implemented. 'visibility': 'public', } # Overwrite default attributes if there are some attributes set metadef_namespace_list.update(attrs) - return metadef_namespace.MetadefNamespace(metadef_namespace_list) + return metadef_namespace.MetadefNamespace(**metadef_namespace_list) diff --git a/openstackclient/tests/unit/image/v2/test_metadef_namespaces.py b/openstackclient/tests/unit/image/v2/test_metadef_namespaces.py index 5eae289c..7ed11838 100644 --- a/openstackclient/tests/unit/image/v2/test_metadef_namespaces.py +++ b/openstackclient/tests/unit/image/v2/test_metadef_namespaces.py @@ -30,8 +30,89 @@ class TestMetadefNamespaces(md_namespace_fakes.TestMetadefNamespaces): self.domain_mock.reset_mock() -class TestMetadefNamespaceList(TestMetadefNamespaces): +class TestMetadefNamespaceCreate(TestMetadefNamespaces): + _metadef_namespace = md_namespace_fakes.create_one_metadef_namespace() + + expected_columns = ( + 'created_at', + 'description', + 'display_name', + 'id', + 'is_protected', + 'location', + 'name', + 'namespace', + 'owner', + 'resource_type_associations', + 'updated_at', + 'visibility' + ) + expected_data = ( + _metadef_namespace.created_at, + _metadef_namespace.description, + _metadef_namespace.display_name, + _metadef_namespace.id, + _metadef_namespace.is_protected, + _metadef_namespace.location, + _metadef_namespace.name, + _metadef_namespace.namespace, + _metadef_namespace.owner, + _metadef_namespace.resource_type_associations, + _metadef_namespace.updated_at, + _metadef_namespace.visibility + ) + + def setUp(self): + super().setUp() + + self.client.create_metadef_namespace.return_value \ + = self._metadef_namespace + self.cmd = metadef_namespaces.CreateMetadefNameSpace(self.app, None) + self.datalist = self._metadef_namespace + + def test_namespace_create(self): + arglist = [ + self._metadef_namespace.namespace + ] + + verifylist = [ + + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_data, data) + + +class TestMetadefNamespaceDelete(TestMetadefNamespaces): + _metadef_namespace = md_namespace_fakes.create_one_metadef_namespace() + + def setUp(self): + super().setUp() + + self.client.delete_metadef_namespace.return_value \ + = self._metadef_namespace + self.cmd = metadef_namespaces.DeleteMetadefNameSpace(self.app, None) + self.datalist = self._metadef_namespace + + def test_namespace_create(self): + arglist = [ + self._metadef_namespace.namespace + ] + + verifylist = [ + + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) + +class TestMetadefNamespaceList(TestMetadefNamespaces): _metadef_namespace = [md_namespace_fakes.create_one_metadef_namespace()] columns = [ @@ -65,3 +146,70 @@ class TestMetadefNamespaceList(TestMetadefNamespaces): self.assertEqual(self.columns, columns) self.assertEqual(getattr(self.datalist[0], 'namespace'), next(data)[0]) + + +class TestMetadefNamespaceSet(TestMetadefNamespaces): + _metadef_namespace = md_namespace_fakes.create_one_metadef_namespace() + + def setUp(self): + super().setUp() + + self.client.update_metadef_namespace.return_value \ + = self._metadef_namespace + self.cmd = metadef_namespaces.SetMetadefNameSpace(self.app, None) + self.datalist = self._metadef_namespace + + def test_namespace_set_no_options(self): + arglist = [ + self._metadef_namespace.namespace + ] + verifylist = [ + ('namespace', self._metadef_namespace.namespace), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) + + +class TestMetadefNamespaceShow(TestMetadefNamespaces): + _metadef_namespace = md_namespace_fakes.create_one_metadef_namespace() + + expected_columns = ( + 'created_at', + 'display_name', + 'namespace', + 'owner', + 'visibility' + ) + expected_data = ( + _metadef_namespace.created_at, + _metadef_namespace.display_name, + _metadef_namespace.namespace, + _metadef_namespace.owner, + _metadef_namespace.visibility + ) + + def setUp(self): + super().setUp() + + self.client.get_metadef_namespace.return_value \ + = self._metadef_namespace + self.cmd = metadef_namespaces.ShowMetadefNameSpace(self.app, None) + + def test_namespace_show_no_options(self): + arglist = [ + self._metadef_namespace.namespace + ] + + verifylist = [ + + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_data, data) diff --git a/releasenotes/notes/add-baremetal-agent-type-7c46365e8d457ac8.yaml b/releasenotes/notes/add-baremetal-agent-type-7c46365e8d457ac8.yaml new file mode 100644 index 00000000..a9a3a0df --- /dev/null +++ b/releasenotes/notes/add-baremetal-agent-type-7c46365e8d457ac8.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``baremetal`` agent type to ``--agent-type`` option for + ``network agent list`` command. diff --git a/releasenotes/notes/image-metadef-namespace-b940206bece64f97.yaml b/releasenotes/notes/image-metadef-namespace-b940206bece64f97.yaml new file mode 100644 index 00000000..361e57fe --- /dev/null +++ b/releasenotes/notes/image-metadef-namespace-b940206bece64f97.yaml @@ -0,0 +1,10 @@ +--- +features: + - Add ``openstack image metadef namespace create`` command + to create metadef namespace for the image service. + - Add ``openstack image metadef namespace delete`` command + to delete image metadef namespace. + - Add ``openstack image metadef namespace set`` command + to update metadef namespace for the image service. + - Add ``openstack image metadef namespace show`` command + to show metadef namespace for the image service. diff --git a/releasenotes/notes/switch-hypervisor-to-sdk-f6495f070b034718.yaml b/releasenotes/notes/switch-hypervisor-to-sdk-f6495f070b034718.yaml new file mode 100644 index 00000000..6f1721b1 --- /dev/null +++ b/releasenotes/notes/switch-hypervisor-to-sdk-f6495f070b034718.yaml @@ -0,0 +1,3 @@ +--- +features: + - Switch hypervisor operations to consume OpenStackSDK diff --git a/releasenotes/notes/switch-server-migration-to-sdk-4e4530f787f90fd2.yaml b/releasenotes/notes/switch-server-migration-to-sdk-4e4530f787f90fd2.yaml new file mode 100644 index 00000000..318ac097 --- /dev/null +++ b/releasenotes/notes/switch-server-migration-to-sdk-4e4530f787f90fd2.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + The ``server migration *`` commands now use the OpenStackSDK instead of + novaclient. diff --git a/releasenotes/notes/switch-server-show-to-sdk-44a614aebf2c6da6.yaml b/releasenotes/notes/switch-server-show-to-sdk-44a614aebf2c6da6.yaml new file mode 100644 index 00000000..c116f6e0 --- /dev/null +++ b/releasenotes/notes/switch-server-show-to-sdk-44a614aebf2c6da6.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + The ``server show`` command now uses the OpenStack SDK instead of the + Python nova bindings. The command prints data fields both by their + novaclient names used in previous releases as well as the names used in the + SDK. @@ -386,7 +386,12 @@ openstack.image.v2 = image_stage = openstackclient.image.v2.image:StageImage image_task_show = openstackclient.image.v2.task:ShowTask image_task_list = openstackclient.image.v2.task:ListTask + + image_metadef_namespace_create = openstackclient.image.v2.metadef_namespaces:CreateMetadefNameSpace + image_metadef_namespace_delete = openstackclient.image.v2.metadef_namespaces:DeleteMetadefNameSpace image_metadef_namespace_list = openstackclient.image.v2.metadef_namespaces:ListMetadefNameSpaces + image_metadef_namespace_set = openstackclient.image.v2.metadef_namespaces:SetMetadefNameSpace + image_metadef_namespace_show = openstackclient.image.v2.metadef_namespaces:ShowMetadefNameSpace openstack.network.v2 = address_group_create = openstackclient.network.v2.address_group:CreateAddressGroup |