From 64e4520b2a79b9046a791f5e3729f5cbfc2d3fa5 Mon Sep 17 00:00:00 2001 From: Nicolas Belouin Date: Fri, 14 Jan 2022 15:44:11 +0100 Subject: [PATCH 001/595] Add trustor and trustee filtering to trusts list The keystone API supports filtering trusts by trustor and/or trustee. Also adds a shortcut parameter to get trusts with current user as trustee or trustor. Signed-off-by: Nicolas Belouin Change-Id: I00ed2b68cf8ada214a59f640f4f0a5c9dbc37063 --- openstackclient/identity/v3/trust.py | 87 +++++++++++++- .../tests/unit/identity/v3/test_trust.py | 108 +++++++++++++++++- ...sing-trust-list-opts-500fd1e4c14e1504.yaml | 9 ++ 3 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/add-missing-trust-list-opts-500fd1e4c14e1504.yaml diff --git a/openstackclient/identity/v3/trust.py b/openstackclient/identity/v3/trust.py index cd3a65d0da..61273f410d 100644 --- a/openstackclient/identity/v3/trust.py +++ b/openstackclient/identity/v3/trust.py @@ -176,10 +176,95 @@ def take_action(self, parsed_args): class ListTrust(command.Lister): _description = _("List trusts") + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + '--trustor', + metavar='', + help=_('Trustor user to filter (name or ID)'), + ) + parser.add_argument( + '--trustee', + metavar='', + help=_('Trustee user to filter (name or ID)'), + ) + parser.add_argument( + '--trustor-domain', + metavar='', + help=_('Domain that contains (name or ID)'), + ) + parser.add_argument( + '--trustee-domain', + metavar='', + help=_('Domain that contains (name or ID)'), + ) + parser.add_argument( + '--auth-user', + action="store_true", + dest='authuser', + help=_('Only list trusts related to the authenticated user'), + ) + return parser + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + auth_ref = self.app.client_manager.auth_ref + + if parsed_args.authuser and any([ + parsed_args.trustor, + parsed_args.trustor_domain, + parsed_args.trustee, + parsed_args.trustee_domain, + ]): + msg = _("--authuser cannot be used with --trustee or --trustor") + raise exceptions.CommandError(msg) + + if parsed_args.trustee_domain and not parsed_args.trustee: + msg = _("Using --trustee-domain mandates the use of --trustee") + raise exceptions.CommandError(msg) + + if parsed_args.trustor_domain and not parsed_args.trustor: + msg = _("Using --trustor-domain mandates the use of --trustor") + raise exceptions.CommandError(msg) + + if parsed_args.authuser: + if auth_ref: + user = common.find_user( + identity_client, + auth_ref.user_id + ) + # We need two calls here as we want trusts with + # either the trustor or the trustee set to current user + # using a single call would give us trusts with both + # trustee and trustor set to current user + data1 = identity_client.trusts.list(trustor_user=user) + data2 = identity_client.trusts.list(trustee_user=user) + data = set(data1 + data2) + else: + trustor = None + if parsed_args.trustor: + trustor = common.find_user( + identity_client, + parsed_args.trustor, + parsed_args.trustor_domain, + ) + + trustee = None + if parsed_args.trustee: + trustee = common.find_user( + identity_client, + parsed_args.trustor, + parsed_args.trustor_domain, + ) + + data = self.app.client_manager.identity.trusts.list( + trustor_user=trustor, + trustee_user=trustee, + ) + columns = ('ID', 'Expires At', 'Impersonation', 'Project ID', 'Trustee User ID', 'Trustor User ID') - data = self.app.client_manager.identity.trusts.list() + return (columns, (utils.get_item_properties( s, columns, diff --git a/openstackclient/tests/unit/identity/v3/test_trust.py b/openstackclient/tests/unit/identity/v3/test_trust.py index d8cfc59fe8..d530adf5a5 100644 --- a/openstackclient/tests/unit/identity/v3/test_trust.py +++ b/openstackclient/tests/unit/identity/v3/test_trust.py @@ -206,7 +206,113 @@ def test_trust_list_no_options(self): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.trusts_mock.list.assert_called_with() + self.trusts_mock.list.assert_called_with( + trustor_user=None, + trustee_user=None, + ) + + collist = ('ID', 'Expires At', 'Impersonation', 'Project ID', + 'Trustee User ID', 'Trustor User ID') + self.assertEqual(collist, columns) + datalist = (( + identity_fakes.trust_id, + identity_fakes.trust_expires, + identity_fakes.trust_impersonation, + identity_fakes.project_id, + identity_fakes.user_id, + identity_fakes.user_id + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_trust_list_auth_user(self): + auth_ref = self.app.client_manager.auth_ref = mock.Mock() + auth_ref.user_id.return_value = identity_fakes.user_id + + arglist = ['--auth-user'] + verifylist = [ + ('trustor', None), + ('trustee', None), + ('authuser', True), + ] + 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) + + self.trusts_mock.list.assert_any_call( + trustor_user=self.users_mock.get() + ) + self.trusts_mock.list.assert_any_call( + trustee_user=self.users_mock.get() + ) + + collist = ('ID', 'Expires At', 'Impersonation', 'Project ID', + 'Trustee User ID', 'Trustor User ID') + self.assertEqual(collist, columns) + datalist = (( + identity_fakes.trust_id, + identity_fakes.trust_expires, + identity_fakes.trust_impersonation, + identity_fakes.project_id, + identity_fakes.user_id, + identity_fakes.user_id + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_trust_list_trustee(self): + arglist = ['--trustee', identity_fakes.user_name] + verifylist = [ + ('trustor', None), + ('trustee', identity_fakes.user_name), + ('authuser', False), + ] + 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) + + print(self.trusts_mock.list.call_args_list) + self.trusts_mock.list.assert_any_call( + trustee_user=self.users_mock.get(), + trustor_user=None, + ) + + collist = ('ID', 'Expires At', 'Impersonation', 'Project ID', + 'Trustee User ID', 'Trustor User ID') + self.assertEqual(collist, columns) + datalist = (( + identity_fakes.trust_id, + identity_fakes.trust_expires, + identity_fakes.trust_impersonation, + identity_fakes.project_id, + identity_fakes.user_id, + identity_fakes.user_id + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_trust_list_trustor(self): + arglist = ['--trustor', identity_fakes.user_name] + verifylist = [ + ('trustee', None), + ('trustor', identity_fakes.user_name), + ('authuser', False), + ] + 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) + + print(self.trusts_mock.list.call_args_list) + self.trusts_mock.list.assert_any_call( + trustor_user=self.users_mock.get(), + trustee_user=None, + ) collist = ('ID', 'Expires At', 'Impersonation', 'Project ID', 'Trustee User ID', 'Trustor User ID') diff --git a/releasenotes/notes/add-missing-trust-list-opts-500fd1e4c14e1504.yaml b/releasenotes/notes/add-missing-trust-list-opts-500fd1e4c14e1504.yaml new file mode 100644 index 0000000000..8918dd8777 --- /dev/null +++ b/releasenotes/notes/add-missing-trust-list-opts-500fd1e4c14e1504.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Add missing ``--trustee``, ``--trustee-domain``, ``--trustor``, + ``--trustor-domain`` options to ``trust list`` command, to allow + filtering trusts by trustee and trustor. + - | + Add ``--authuser`` option to ``trust list`` command, to allow + displaying only trusts related to current authenticated user From 725b7de13cf00da386132a42b2738f4c57026184 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 12 Apr 2022 20:16:59 +0100 Subject: [PATCH 002/595] compute: Only retrieve necessary images The Glance API allows us to filter by multiple IDs using the 'in:' operator. Take advantage of this to speed up listing of server in larger deployments where image counts in the hundreds (or even thousands) are not uncommon. Unfortunately the Nova API does not support something similar for listing flavors. Boo. Change-Id: I7d3222d0b0b8bf72b4ff3e429bc49e621b569979 Signed-off-by: Stephen Finucane Depends-On: https://review.opendev.org/c/openstack/openstacksdk/+/837613 --- openstackclient/compute/v2/server.py | 31 ++++++++++++++----- .../tests/unit/compute/v2/test_server.py | 24 +++++++++++--- ...list-restrict-images-c0b2c4de6f93df33.yaml | 8 +++++ 3 files changed, 51 insertions(+), 12 deletions(-) create mode 100644 releasenotes/notes/server-list-restrict-images-c0b2c4de6f93df33.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 69aaa3c57b..803887359c 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2479,28 +2479,45 @@ def take_action(self, parsed_args): data = compute_client.servers.list( search_opts=search_opts, marker=marker_id, - limit=parsed_args.limit) + limit=parsed_args.limit, + ) images = {} flavors = {} if data and not parsed_args.no_name_lookup: + # partial responses from down cells will not have an image + # attribute so we use getattr + image_ids = { + s.image['id'] for s in data + if getattr(s, 'image', None) and s.image.get('id') + } + # create a dict that maps image_id to image object, which is used # to display the "Image Name" column. Note that 'image.id' can be # empty for BFV instances and 'image' can be missing entirely if # there are infra failures if parsed_args.name_lookup_one_by_one or image_id: - for i_id in set( - s.image['id'] for s in data - if s.image and s.image.get('id') - ): + for image_id in image_ids: # "Image Name" is not crucial, so we swallow any exceptions try: - images[i_id] = image_client.get_image(i_id) + images[image_id] = image_client.get_image(image_id) except Exception: pass else: try: - images_list = image_client.images() + # some deployments can have *loads* of images so we only + # want to list the ones we care about. It would be better + # to only retrun the *fields* we care about (name) but + # glance doesn't support that + # NOTE(stephenfin): This could result in super long URLs + # but it seems unlikely to cause issues. Apache supports + # URL lengths of up to 8190 characters by default, which + # should allow for more than 220 unique image ID (different + # servers are likely use the same image ID) in the filter. + # Who'd need more than that in a single command? + images_list = image_client.images( + id=f"in:{','.join(image_ids)}" + ) for i in images_list: images[i.id] = i except Exception: diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 46ace57903..207daf4b3c 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -4441,8 +4441,8 @@ def test_server_list_no_servers(self): columns, data = self.cmd.take_action(parsed_args) self.servers_mock.list.assert_called_with(**self.kwargs) - self.assertEqual(0, self.images_mock.list.call_count) - self.assertEqual(0, self.flavors_mock.list.call_count) + self.images_mock.assert_not_called() + self.flavors_mock.list.assert_not_called() self.assertEqual(self.columns, columns) self.assertEqual(self.data, tuple(data)) @@ -4465,7 +4465,8 @@ def test_server_list_long_option(self): getattr(s, 'OS-EXT-AZ:availability_zone'), getattr(s, 'OS-EXT-SRV-ATTR:host'), s.Metadata, - ) for s in self.servers) + ) for s in self.servers + ) arglist = [ '--long', ] @@ -4477,6 +4478,11 @@ def test_server_list_long_option(self): columns, data = self.cmd.take_action(parsed_args) self.servers_mock.list.assert_called_with(**self.kwargs) + image_ids = {s.image['id'] for s in self.servers if s.image} + self.images_mock.assert_called_once_with( + id=f'in:{",".join(image_ids)}', + ) + self.flavors_mock.list.assert_called_once_with(is_public=None) self.assertEqual(self.columns_long, columns) self.assertEqual(self.data, tuple(data)) @@ -4526,6 +4532,8 @@ def test_server_list_no_name_lookup_option(self): columns, data = self.cmd.take_action(parsed_args) self.servers_mock.list.assert_called_with(**self.kwargs) + self.images_mock.assert_not_called() + self.flavors_mock.list.assert_not_called() self.assertEqual(self.columns, columns) self.assertEqual(self.data, tuple(data)) @@ -4554,6 +4562,8 @@ def test_server_list_n_option(self): columns, data = self.cmd.take_action(parsed_args) self.servers_mock.list.assert_called_with(**self.kwargs) + self.images_mock.assert_not_called() + self.flavors_mock.list.assert_not_called() self.assertEqual(self.columns, columns) self.assertEqual(self.data, tuple(data)) @@ -4571,8 +4581,8 @@ def test_server_list_name_lookup_one_by_one(self): columns, data = self.cmd.take_action(parsed_args) self.servers_mock.list.assert_called_with(**self.kwargs) - self.assertFalse(self.images_mock.list.call_count) - self.assertFalse(self.flavors_mock.list.call_count) + self.images_mock.assert_not_called() + self.flavors_mock.list.assert_not_called() self.get_image_mock.assert_called() self.flavors_mock.get.assert_called() @@ -4596,6 +4606,8 @@ def test_server_list_with_image(self): self.search_opts['image'] = self.image.id self.servers_mock.list.assert_called_with(**self.kwargs) + self.images_mock.assert_not_called() + self.flavors_mock.list.assert_called_once() self.assertEqual(self.columns, columns) self.assertEqual(self.data, tuple(data)) @@ -4616,6 +4628,8 @@ def test_server_list_with_flavor(self): self.search_opts['flavor'] = self.flavor.id self.servers_mock.list.assert_called_with(**self.kwargs) + self.images_mock.assert_called_once() + self.flavors_mock.list.assert_not_called() self.assertEqual(self.columns, columns) self.assertEqual(self.data, tuple(data)) diff --git a/releasenotes/notes/server-list-restrict-images-c0b2c4de6f93df33.yaml b/releasenotes/notes/server-list-restrict-images-c0b2c4de6f93df33.yaml new file mode 100644 index 0000000000..2ee2e4d87b --- /dev/null +++ b/releasenotes/notes/server-list-restrict-images-c0b2c4de6f93df33.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + The ``server list`` needs to query the image service API to retrieve + image names as part of the response. This command will now retrieve only + the images that are relevant, i.e. those used by the server included in + the output. This should result in signficantly faster responses when + using a deployment with a large number of public images. From 722d3216a4b5d13aa1e258892d4c660a79ee91c9 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 7 Jul 2022 13:39:00 +0100 Subject: [PATCH 003/595] docs: Add missing command mappings The 'neutron tag-replace' command is equivalent to the 'openstack network set --no-tag --tag [--tag ...]' command. '--no-tag' will unset all tags while '--tag ' will set a new tag and can be specified multiple times. The 'nova hypervisor-uptime' command is equivalent to the 'openstack hypervisor show' command. Before compute microversion 2.88, the nova command was using a different API that returned an almost identical output to 'nova hypervisor-show' except it included an 'uptime' field. Since 2.88, this field is returned in the standard call. OSC abstracts this detail away so the 'uptime' field is always present, removing the need for a separate command. The 'nova migration-list' command is implemented as 'openstack server migration list' (simply omit the '--server' filter). The 'nova volume-update' command is implemented as 'openstack server volume update'. The 'nova volume-attachments' command is actually available as 'openstack server volume list', though you can also use 'server show'. The 'nova aggregate-cache-images' corresponds to an internal API that only glance should be using. It doesn't need to be exposed via openstackclient. Change-Id: Icdbc42762230954f6f7f2064b6228416af41d45a Signed-off-by: Stephen Finucane --- doc/source/cli/data/neutron.csv | 2 +- doc/source/cli/data/nova.csv | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/source/cli/data/neutron.csv b/doc/source/cli/data/neutron.csv index 402f4064ee..15de30e614 100644 --- a/doc/source/cli/data/neutron.csv +++ b/doc/source/cli/data/neutron.csv @@ -230,7 +230,7 @@ subnetpool-show,subnet pool show,Show information of a given subnetpool. subnetpool-update,subnet pool set / subnet pool unset,Update subnetpool's information. tag-add,network set --tag,Add a tag into the resource. tag-remove,network unset --tag,Remove a tag on the resource. -tag-replace,,Replace all tags on the resource. +tag-replace,network set --no-tag --tag,Replace all tags on the resource. vpn-endpoint-group-create,,Create a VPN endpoint group. vpn-endpoint-group-delete,,Delete a given VPN endpoint group. vpn-endpoint-group-list,,List VPN endpoint groups that belong to a given tenant. diff --git a/doc/source/cli/data/nova.csv b/doc/source/cli/data/nova.csv index 0ab2b2fee4..c6e9199847 100644 --- a/doc/source/cli/data/nova.csv +++ b/doc/source/cli/data/nova.csv @@ -4,7 +4,7 @@ agent-delete,compute agent delete,Delete existing agent build. agent-list,compute agent list,List all builds. agent-modify,compute agent set,Modify existing agent build. aggregate-add-host,aggregate add host,Add the host to the specified aggregate. -aggregate-cache-images,,Request images be cached. (Supported by API versions '2.81' - '2.latest') [hint: use '-- os-compute-api-version' flag to show help message for proper version] +aggregate-cache-images,WONTFIX,Request images be cached. (Supported by API versions '2.81' - '2.latest') [hint: use '-- os-compute-api-version' flag to show help message for proper version] aggregate-create,aggregate create,Create a new aggregate with the specified details. aggregate-delete,aggregate delete,Delete the aggregate. aggregate-list,aggregate list,Print a list of all aggregates. @@ -44,7 +44,7 @@ hypervisor-list,hypervisor list,List hypervisors. (Supported by API versions '2. hypervisor-servers,,List servers belonging to specific hypervisors. hypervisor-show,hypervisor show,Display the details of the specified hypervisor. hypervisor-stats,hypervisor stats show,Get hypervisor statistics over all compute nodes. -hypervisor-uptime,,Display the uptime of the specified hypervisor. +hypervisor-uptime,hypervisor show,Display the uptime of the specified hypervisor. image-create,server image create,Create a new image by taking a snapshot of a running server. instance-action,server event show,Show an action. instance-action-list,,List actions on a server. @@ -65,7 +65,7 @@ live-migration-force-comp,server migration force complete,Force on-going live mi lock,server lock,Lock a server. meta,server set --property / unset,Set or delete metadata on a server. migrate,server migrate,Migrate a server. The new host will be selected by the scheduler. -migration-list,,Print a list of migrations. +migration-list,server migration list,Print a list of migrations. pause,server pause,Pause a server. quota-class-show,quota show --class,List the quotas for a quota class. quota-class-update,quota set --class,Update the quotas for a quota class. @@ -120,8 +120,8 @@ usage,usage show,Show usage data for a single tenant. usage-list,usage list,List usage data for all tenants. version-list,,List all API versions. volume-attach,server add volume,Attach a volume to a server. -volume-attachments,server show,List all the volumes attached to a server. +volume-attachments,server volume list,List all the volumes attached to a server. volume-detach,server remove volume,Detach a volume from a server. -volume-update,,Update volume attachment. +volume-update,server volume update,Update volume attachment. bash-completion,complete,Prints all of the commands and options to stdout so that the nova.bash_completion script doesn't have to hard code them. help,help,Display help about this program or one of its subcommands. From bced4852c7a92959f5457360dd91eb397af4d270 Mon Sep 17 00:00:00 2001 From: Pedro Martins Date: Sat, 31 Aug 2019 20:10:47 -0300 Subject: [PATCH 004/595] Add port ranges on floating ip portforwardings cli This patch is one of a series of patches to implement floating ip port forwarding with port ranges. The specification is defined in: https://github.com/openstack/neutron-specs/blob/master/specs/wallaby/port-forwarding-port-ranges.rst Change-Id: If9679c87fd8b770fcd960048e091ee8d59205285 Implements: blueprint floatingips-portforwarding-ranges Related-Bug: #1885921 --- .../network/v2/floating_ip_port_forwarding.py | 102 +++++--- .../tests/unit/network/v2/fakes.py | 38 ++- .../v2/test_floating_ip_port_forwarding.py | 231 ++++++++++++++++-- ...t-forwarding-command-8c6ee05cf625578a.yaml | 4 + 4 files changed, 318 insertions(+), 57 deletions(-) create mode 100644 releasenotes/notes/add-port-ranges-in-port-forwarding-command-8c6ee05cf625578a.yaml diff --git a/openstackclient/network/v2/floating_ip_port_forwarding.py b/openstackclient/network/v2/floating_ip_port_forwarding.py index b33633d346..cd71c05a9e 100644 --- a/openstackclient/network/v2/floating_ip_port_forwarding.py +++ b/openstackclient/network/v2/floating_ip_port_forwarding.py @@ -25,6 +25,61 @@ LOG = logging.getLogger(__name__) +def validate_ports_diff(ports): + if len(ports) == 0: + return 0 + + ports_diff = ports[-1] - ports[0] + if ports_diff < 0: + msg = _("The last number in port range must be" + " greater or equal to the first") + raise exceptions.CommandError(msg) + return ports_diff + + +def validate_ports_match(internal_ports, external_ports): + internal_ports_diff = validate_ports_diff(internal_ports) + external_ports_diff = validate_ports_diff(external_ports) + + if internal_ports_diff != 0 and internal_ports_diff != external_ports_diff: + msg = _("The relation between internal and external ports does not " + "match the pattern 1:N and N:N") + raise exceptions.CommandError(msg) + + +def validate_and_assign_port_ranges(parsed_args, attrs): + internal_port_range = parsed_args.internal_protocol_port + external_port_range = parsed_args.external_protocol_port + external_ports = internal_ports = [] + if external_port_range: + external_ports = list(map(int, str(external_port_range).split(':'))) + if internal_port_range: + internal_ports = list(map(int, str(internal_port_range).split(':'))) + + validate_ports_match(internal_ports, external_ports) + + for port in external_ports + internal_ports: + validate_port(port) + + if internal_port_range: + if ':' in internal_port_range: + attrs['internal_port_range'] = internal_port_range + else: + attrs['internal_port'] = int(internal_port_range) + + if external_port_range: + if ':' in external_port_range: + attrs['external_port_range'] = external_port_range + else: + attrs['external_port'] = int(external_port_range) + + +def validate_port(port): + if port <= 0 or port > 65535: + msg = _("The port number range is <1-65535>") + raise exceptions.CommandError(msg) + + def _get_columns(item): column_map = {} hidden_columns = ['location'] @@ -58,7 +113,6 @@ def get_parser(self, prog_name): ) parser.add_argument( '--internal-protocol-port', - type=int, metavar='', required=True, help=_("The protocol port number " @@ -67,7 +121,6 @@ def get_parser(self, prog_name): ) parser.add_argument( '--external-protocol-port', - type=int, metavar='', required=True, help=_("The protocol port number of " @@ -92,6 +145,7 @@ def get_parser(self, prog_name): help=_("Floating IP that the port forwarding belongs to " "(IP address or ID)") ) + return parser def take_action(self, parsed_args): @@ -102,19 +156,7 @@ def take_action(self, parsed_args): ignore_missing=False, ) - if parsed_args.internal_protocol_port is not None: - if (parsed_args.internal_protocol_port <= 0 or - parsed_args.internal_protocol_port > 65535): - msg = _("The port number range is <1-65535>") - raise exceptions.CommandError(msg) - attrs['internal_port'] = parsed_args.internal_protocol_port - - if parsed_args.external_protocol_port is not None: - if (parsed_args.external_protocol_port <= 0 or - parsed_args.external_protocol_port > 65535): - msg = _("The port number range is <1-65535>") - raise exceptions.CommandError(msg) - attrs['external_port'] = parsed_args.external_protocol_port + validate_and_assign_port_ranges(parsed_args, attrs) if parsed_args.port: port = client.find_port(parsed_args.port, @@ -226,7 +268,9 @@ def take_action(self, parsed_args): 'internal_port_id', 'internal_ip_address', 'internal_port', + 'internal_port_range', 'external_port', + 'external_port_range', 'protocol', 'description', ) @@ -235,7 +279,9 @@ def take_action(self, parsed_args): 'Internal Port ID', 'Internal IP Address', 'Internal Port', + 'Internal Port Range', 'External Port', + 'External Port Range', 'Protocol', 'Description', ) @@ -246,8 +292,13 @@ def take_action(self, parsed_args): port = client.find_port(parsed_args.port, ignore_missing=False) query['internal_port_id'] = port.id - if parsed_args.external_protocol_port is not None: - query['external_port'] = parsed_args.external_protocol_port + external_port = parsed_args.external_protocol_port + if external_port: + if ':' in external_port: + query['external_port_range'] = external_port + else: + query['external_port'] = int( + parsed_args.external_protocol_port) if parsed_args.protocol is not None: query['protocol'] = parsed_args.protocol @@ -297,14 +348,12 @@ def get_parser(self, prog_name): parser.add_argument( '--internal-protocol-port', metavar='', - type=int, help=_("The TCP/UDP/other protocol port number of the " "network port fixed IPv4 address associated to " "the floating IP port forwarding") ) parser.add_argument( '--external-protocol-port', - type=int, metavar='', help=_("The TCP/UDP/other protocol port number of the " "port forwarding's floating IP address") @@ -339,19 +388,8 @@ def take_action(self, parsed_args): if parsed_args.internal_ip_address: attrs['internal_ip_address'] = parsed_args.internal_ip_address - if parsed_args.internal_protocol_port is not None: - if (parsed_args.internal_protocol_port <= 0 or - parsed_args.internal_protocol_port > 65535): - msg = _("The port number range is <1-65535>") - raise exceptions.CommandError(msg) - attrs['internal_port'] = parsed_args.internal_protocol_port - - if parsed_args.external_protocol_port is not None: - if (parsed_args.external_protocol_port <= 0 or - parsed_args.external_protocol_port > 65535): - msg = _("The port number range is <1-65535>") - raise exceptions.CommandError(msg) - attrs['external_port'] = parsed_args.external_protocol_port + + validate_and_assign_port_ranges(parsed_args, attrs) if parsed_args.protocol: attrs['protocol'] = parsed_args.protocol diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index 912f451fea..bb113d3c95 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -1346,11 +1346,13 @@ class FakeFloatingIPPortForwarding(object): """"Fake one or more Port forwarding""" @staticmethod - def create_one_port_forwarding(attrs=None): + def create_one_port_forwarding(attrs=None, use_range=False): """Create a fake Port Forwarding. :param Dictionary attrs: A dictionary with all attributes + :param Boolean use_range: + A boolean which defines if we will use ranges or not :return: A FakeResource object with name, id, etc. """ @@ -1364,13 +1366,29 @@ def create_one_port_forwarding(attrs=None): 'floatingip_id': floatingip_id, 'internal_port_id': 'internal-port-id-' + uuid.uuid4().hex, 'internal_ip_address': '192.168.1.2', - 'internal_port': randint(1, 65535), - 'external_port': randint(1, 65535), 'protocol': 'tcp', 'description': 'some description', 'location': 'MUNCHMUNCHMUNCH', } + if use_range: + port_range = randint(0, 100) + internal_start = randint(1, 65535 - port_range) + internal_end = internal_start + port_range + internal_range = ':'.join(map(str, [internal_start, internal_end])) + external_start = randint(1, 65535 - port_range) + external_end = external_start + port_range + external_range = ':'.join(map(str, [external_start, external_end])) + port_forwarding_attrs['internal_port_range'] = internal_range + port_forwarding_attrs['external_port_range'] = external_range + port_forwarding_attrs['internal_port'] = None + port_forwarding_attrs['external_port'] = None + else: + port_forwarding_attrs['internal_port'] = randint(1, 65535) + port_forwarding_attrs['external_port'] = randint(1, 65535) + port_forwarding_attrs['internal_port_range'] = '' + port_forwarding_attrs['external_port_range'] = '' + # Overwrite default attributes. port_forwarding_attrs.update(attrs) @@ -1381,25 +1399,28 @@ def create_one_port_forwarding(attrs=None): return port_forwarding @staticmethod - def create_port_forwardings(attrs=None, count=2): + def create_port_forwardings(attrs=None, count=2, use_range=False): """Create multiple fake Port Forwarding. :param Dictionary attrs: A dictionary with all attributes :param int count: The number of Port Forwarding rule to fake + :param Boolean use_range: + A boolean which defines if we will use ranges or not :return: A list of FakeResource objects faking the Port Forwardings """ port_forwardings = [] for i in range(0, count): port_forwardings.append( - FakeFloatingIPPortForwarding.create_one_port_forwarding(attrs) + FakeFloatingIPPortForwarding.create_one_port_forwarding( + attrs, use_range=use_range) ) return port_forwardings @staticmethod - def get_port_forwardings(port_forwardings=None, count=2): + def get_port_forwardings(port_forwardings=None, count=2, use_range=False): """Get a list of faked Port Forwardings. If port forwardings list is provided, then initialize the Mock object @@ -1409,13 +1430,16 @@ def get_port_forwardings(port_forwardings=None, count=2): A list of FakeResource objects faking port forwardings :param int count: The number of Port Forwardings to fake + :param Boolean use_range: + A boolean which defines if we will use ranges or not :return: An iterable Mock object with side_effect set to a list of faked Port Forwardings """ if port_forwardings is None: port_forwardings = ( - FakeFloatingIPPortForwarding.create_port_forwardings(count) + FakeFloatingIPPortForwarding.create_port_forwardings( + count, use_range=use_range) ) return mock.Mock(side_effect=port_forwardings) diff --git a/openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py b/openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py index 7b9e3aa6e7..4c82fd17b7 100644 --- a/openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py +++ b/openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py @@ -49,6 +49,18 @@ def setUp(self): } ) ) + + self.new_port_forwarding_with_ranges = ( + network_fakes.FakeFloatingIPPortForwarding. + create_one_port_forwarding( + use_range=True, + attrs={ + 'internal_port_id': self.port.id, + 'floatingip_id': self.floating_ip.id, + } + ) + ) + self.network.create_floating_ip_port_forwarding = mock.Mock( return_value=self.new_port_forwarding) @@ -63,22 +75,26 @@ def setUp(self): self.columns = ( 'description', 'external_port', + 'external_port_range', 'floatingip_id', 'id', 'internal_ip_address', 'internal_port', 'internal_port_id', + 'internal_port_range', 'protocol' ) self.data = ( self.new_port_forwarding.description, self.new_port_forwarding.external_port, + self.new_port_forwarding.external_port_range, self.new_port_forwarding.floatingip_id, self.new_port_forwarding.id, self.new_port_forwarding.internal_ip_address, self.new_port_forwarding.internal_port, self.new_port_forwarding.internal_port_id, + self.new_port_forwarding.internal_port_range, self.new_port_forwarding.protocol, ) @@ -90,6 +106,160 @@ def test_create_no_options(self): self.assertRaises(tests_utils.ParserException, self.check_parser, self.cmd, arglist, verifylist) + def test_create_all_options_with_range(self): + arglist = [ + '--port', self.new_port_forwarding_with_ranges.internal_port_id, + '--internal-protocol-port', + self.new_port_forwarding_with_ranges.internal_port_range, + '--external-protocol-port', + self.new_port_forwarding_with_ranges.external_port_range, + '--protocol', self.new_port_forwarding_with_ranges.protocol, + self.new_port_forwarding_with_ranges.floatingip_id, + '--internal-ip-address', + self.new_port_forwarding_with_ranges.internal_ip_address, + '--description', + self.new_port_forwarding_with_ranges.description, + ] + verifylist = [ + ('port', self.new_port_forwarding_with_ranges.internal_port_id), + ('internal_protocol_port', + self.new_port_forwarding_with_ranges.internal_port_range), + ('external_protocol_port', + self.new_port_forwarding_with_ranges.external_port_range), + ('protocol', self.new_port_forwarding_with_ranges.protocol), + ('floating_ip', + self.new_port_forwarding_with_ranges.floatingip_id), + ('internal_ip_address', self.new_port_forwarding_with_ranges. + internal_ip_address), + ('description', self.new_port_forwarding_with_ranges.description), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_floating_ip_port_forwarding.\ + assert_called_once_with( + self.new_port_forwarding.floatingip_id, + **{ + 'external_port_range': + self.new_port_forwarding_with_ranges. + external_port_range, + 'internal_ip_address': + self.new_port_forwarding_with_ranges. + internal_ip_address, + 'internal_port_range': + self.new_port_forwarding_with_ranges. + internal_port_range, + 'internal_port_id': + self.new_port_forwarding_with_ranges.internal_port_id, + 'protocol': self.new_port_forwarding_with_ranges.protocol, + 'description': + self.new_port_forwarding_with_ranges.description, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_all_options_with_range_invalid_port_exception(self): + invalid_port_range = '999999:999999' + arglist = [ + '--port', self.new_port_forwarding_with_ranges.internal_port_id, + '--internal-protocol-port', invalid_port_range, + '--external-protocol-port', invalid_port_range, + '--protocol', self.new_port_forwarding_with_ranges.protocol, + self.new_port_forwarding_with_ranges.floatingip_id, + '--internal-ip-address', + self.new_port_forwarding_with_ranges.internal_ip_address, + '--description', + self.new_port_forwarding_with_ranges.description, + ] + verifylist = [ + ('port', self.new_port_forwarding_with_ranges.internal_port_id), + ('internal_protocol_port', invalid_port_range), + ('external_protocol_port', invalid_port_range), + ('protocol', self.new_port_forwarding_with_ranges.protocol), + ('floating_ip', + self.new_port_forwarding_with_ranges.floatingip_id), + ('internal_ip_address', self.new_port_forwarding_with_ranges. + internal_ip_address), + ('description', self.new_port_forwarding_with_ranges.description), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + msg = 'The port number range is <1-65535>' + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual(msg, str(e)) + self.network.create_floating_ip_port_forwarding.assert_not_called() + + def test_create_all_options_with_invalid_range_exception(self): + invalid_port_range = '80:70' + arglist = [ + '--port', self.new_port_forwarding_with_ranges.internal_port_id, + '--internal-protocol-port', invalid_port_range, + '--external-protocol-port', invalid_port_range, + '--protocol', self.new_port_forwarding_with_ranges.protocol, + self.new_port_forwarding_with_ranges.floatingip_id, + '--internal-ip-address', + self.new_port_forwarding_with_ranges.internal_ip_address, + '--description', + self.new_port_forwarding_with_ranges.description, + ] + verifylist = [ + ('port', self.new_port_forwarding_with_ranges.internal_port_id), + ('internal_protocol_port', invalid_port_range), + ('external_protocol_port', invalid_port_range), + ('protocol', self.new_port_forwarding_with_ranges.protocol), + ('floating_ip', + self.new_port_forwarding_with_ranges.floatingip_id), + ('internal_ip_address', self.new_port_forwarding_with_ranges. + internal_ip_address), + ('description', self.new_port_forwarding_with_ranges.description), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + msg = 'The last number in port range must be greater or equal to ' \ + 'the first' + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual(msg, str(e)) + self.network.create_floating_ip_port_forwarding.assert_not_called() + + def test_create_all_options_with_unmatch_ranges_exception(self): + internal_range = '80:90' + external_range = '8080:8100' + arglist = [ + '--port', self.new_port_forwarding_with_ranges.internal_port_id, + '--internal-protocol-port', internal_range, + '--external-protocol-port', external_range, + '--protocol', self.new_port_forwarding_with_ranges.protocol, + self.new_port_forwarding_with_ranges.floatingip_id, + '--internal-ip-address', + self.new_port_forwarding_with_ranges.internal_ip_address, + '--description', + self.new_port_forwarding_with_ranges.description, + ] + verifylist = [ + ('port', self.new_port_forwarding_with_ranges.internal_port_id), + ('internal_protocol_port', internal_range), + ('external_protocol_port', external_range), + ('protocol', self.new_port_forwarding_with_ranges.protocol), + ('floating_ip', + self.new_port_forwarding_with_ranges.floatingip_id), + ('internal_ip_address', self.new_port_forwarding_with_ranges. + internal_ip_address), + ('description', self.new_port_forwarding_with_ranges.description), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + msg = "The relation between internal and external ports does not " \ + "match the pattern 1:N and N:N" + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual(msg, str(e)) + self.network.create_floating_ip_port_forwarding.assert_not_called() + def test_create_all_options(self): arglist = [ '--port', self.new_port_forwarding.internal_port_id, @@ -106,8 +276,10 @@ def test_create_all_options(self): ] verifylist = [ ('port', self.new_port_forwarding.internal_port_id), - ('internal_protocol_port', self.new_port_forwarding.internal_port), - ('external_protocol_port', self.new_port_forwarding.external_port), + ('internal_protocol_port', + str(self.new_port_forwarding.internal_port)), + ('external_protocol_port', + str(self.new_port_forwarding.external_port)), ('protocol', self.new_port_forwarding.protocol), ('floating_ip', self.new_port_forwarding.floatingip_id), ('internal_ip_address', self.new_port_forwarding. @@ -253,7 +425,9 @@ class TestListFloatingIPPortForwarding(TestFloatingIPPortForwarding): 'Internal Port ID', 'Internal IP Address', 'Internal Port', + 'Internal Port Range', 'External Port', + 'External Port Range', 'Protocol', 'Description', ) @@ -275,7 +449,9 @@ def setUp(self): port_forwarding.internal_port_id, port_forwarding.internal_ip_address, port_forwarding.internal_port, + port_forwarding.internal_port_range, port_forwarding.external_port, + port_forwarding.external_port_range, port_forwarding.protocol, port_forwarding.description, )) @@ -330,7 +506,7 @@ def test_port_forwarding_list_all_options(self): query = { 'internal_port_id': self.port_forwardings[0].internal_port_id, - 'external_port': str(self.port_forwardings[0].external_port), + 'external_port': self.port_forwardings[0].external_port, 'protocol': self.port_forwardings[0].protocol, } @@ -392,7 +568,7 @@ def test_set_nothing(self): self.assertIsNone(result) def test_set_all_thing(self): - arglist = [ + arglist_single = [ '--port', self.port.id, '--internal-ip-address', 'new_internal_ip_address', '--internal-protocol-port', '100', @@ -402,21 +578,23 @@ def test_set_all_thing(self): self._port_forwarding.floatingip_id, self._port_forwarding.id, ] - verifylist = [ + arglist_range = list(arglist_single) + arglist_range[5] = '100:110' + arglist_range[7] = '200:210' + verifylist_single = [ ('port', self.port.id), ('internal_ip_address', 'new_internal_ip_address'), - ('internal_protocol_port', 100), - ('external_protocol_port', 200), + ('internal_protocol_port', '100'), + ('external_protocol_port', '200'), ('protocol', 'tcp'), ('description', 'some description'), ('floating_ip', self._port_forwarding.floatingip_id), ('port_forwarding_id', self._port_forwarding.id), ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - result = self.cmd.take_action(parsed_args) - attrs = { + verifylist_range = list(verifylist_single) + verifylist_range[2] = ('internal_protocol_port', '100:110') + verifylist_range[3] = ('external_protocol_port', '200:210') + attrs_single = { 'internal_port_id': self.port.id, 'internal_ip_address': 'new_internal_ip_address', 'internal_port': 100, @@ -424,12 +602,25 @@ def test_set_all_thing(self): 'protocol': 'tcp', 'description': 'some description', } - self.network.update_floating_ip_port_forwarding.assert_called_with( - self._port_forwarding.floatingip_id, - self._port_forwarding.id, - **attrs - ) - self.assertIsNone(result) + attrs_range = dict(attrs_single, internal_port_range='100:110', + external_port_range='200:210') + attrs_range.pop('internal_port') + attrs_range.pop('external_port') + + def run_and_validate(arglist, verifylist, attrs): + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.update_floating_ip_port_forwarding.assert_called_with( + self._port_forwarding.floatingip_id, + self._port_forwarding.id, + **attrs + ) + self.assertIsNone(result) + + run_and_validate(arglist_single, verifylist_single, attrs_single) + run_and_validate(arglist_range, verifylist_range, attrs_range) class TestShowFloatingIPPortForwarding(TestFloatingIPPortForwarding): @@ -438,11 +629,13 @@ class TestShowFloatingIPPortForwarding(TestFloatingIPPortForwarding): columns = ( 'description', 'external_port', + 'external_port_range', 'floatingip_id', 'id', 'internal_ip_address', 'internal_port', 'internal_port_id', + 'internal_port_range', 'protocol', ) @@ -459,11 +652,13 @@ def setUp(self): self.data = ( self._port_forwarding.description, self._port_forwarding.external_port, + self._port_forwarding.external_port_range, self._port_forwarding.floatingip_id, self._port_forwarding.id, self._port_forwarding.internal_ip_address, self._port_forwarding.internal_port, self._port_forwarding.internal_port_id, + self._port_forwarding.internal_port_range, self._port_forwarding.protocol, ) self.network.find_floating_ip_port_forwarding = mock.Mock( diff --git a/releasenotes/notes/add-port-ranges-in-port-forwarding-command-8c6ee05cf625578a.yaml b/releasenotes/notes/add-port-ranges-in-port-forwarding-command-8c6ee05cf625578a.yaml new file mode 100644 index 0000000000..80e4445e32 --- /dev/null +++ b/releasenotes/notes/add-port-ranges-in-port-forwarding-command-8c6ee05cf625578a.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add port ranges support to the ``floating ip port forwarding`` commands. From 167cf11e825af95fe40c1daefdb6095c791a3ee5 Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Wed, 13 Jul 2022 15:09:34 +0300 Subject: [PATCH 005/595] Add authorization_ttl for identity providers this is supported since Ussuri (Keystone API version 3.14) but was lacking from openstackclient. Change-Id: Ifac818b9a4eff66d9a68455ada1ddfe67cb46b3b --- .../identity/v3/identity_provider.py | 45 ++++- .../identity/v3/test_identity_provider.py | 170 ++++++++++++++++++ .../notes/idp-auth-ttl-6632df5db65a3bdd.yaml | 9 + 3 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/idp-auth-ttl-6632df5db65a3bdd.yaml diff --git a/openstackclient/identity/v3/identity_provider.py b/openstackclient/identity/v3/identity_provider.py index 7307cea00c..19a6214487 100644 --- a/openstackclient/identity/v3/identity_provider.py +++ b/openstackclient/identity/v3/identity_provider.py @@ -63,6 +63,16 @@ def get_parser(self, prog_name): 'specified, a domain will be created automatically. ' '(Name or ID)'), ) + parser.add_argument( + '--authorization-ttl', + metavar='', + type=int, + help=_('Time to keep the role assignments for users ' + 'authenticating via this identity provider. ' + 'When not provided, global default configured in the ' + 'Identity service will be used. ' + 'Available since Identity API version 3.14 (Ussuri).'), + ) enable_identity_provider = parser.add_mutually_exclusive_group() enable_identity_provider.add_argument( '--enable', @@ -95,12 +105,23 @@ def take_action(self, parsed_args): domain_id = common.find_domain(identity_client, parsed_args.domain).id + # TODO(pas-ha) actually check for 3.14 microversion + kwargs = {} + auth_ttl = parsed_args.authorization_ttl + if auth_ttl is not None: + if auth_ttl < 0: + msg = (_("%(param)s must be positive integer or zero." + ) % {"param": "authorization-ttl"}) + raise exceptions.CommandError(msg) + kwargs['authorization_ttl'] = auth_ttl + idp = identity_client.federation.identity_providers.create( id=parsed_args.identity_provider_id, remote_ids=remote_ids, description=parsed_args.description, domain_id=domain_id, - enabled=parsed_args.enabled) + enabled=parsed_args.enabled, + **kwargs) idp._info.pop('links', None) remote_ids = format_columns.ListColumn(idp._info.pop('remote_ids', [])) @@ -205,6 +226,14 @@ def get_parser(self, prog_name): help=_('Name of a file that contains many remote IDs to associate ' 'with the identity provider, one per line'), ) + parser.add_argument( + '--authorization-ttl', + metavar='', + type=int, + help=_('Time to keep the role assignments for users ' + 'authenticating via this identity provider. ' + 'Available since Identity API version 3.14 (Ussuri).'), + ) enable_identity_provider = parser.add_mutually_exclusive_group() enable_identity_provider.add_argument( '--enable', @@ -241,6 +270,20 @@ def take_action(self, parsed_args): if parsed_args.remote_id_file or parsed_args.remote_id: kwargs['remote_ids'] = remote_ids + # TODO(pas-ha) actually check for 3.14 microversion + # TODO(pas-ha) make it possible to reset authorization_ttl + # back to None value. + # Currently not possible as filter_kwargs decorator in + # keystoneclient/base.py explicitly drops the None-valued keys + # from kwargs, and 'update' method is wrapped in this decorator. + auth_ttl = parsed_args.authorization_ttl + if auth_ttl is not None: + if auth_ttl < 0: + msg = (_("%(param)s must be positive integer or zero." + ) % {"param": "authorization-ttl"}) + raise exceptions.CommandError(msg) + kwargs['authorization_ttl'] = auth_ttl + federation_client.identity_providers.update( parsed_args.identity_provider, **kwargs diff --git a/openstackclient/tests/unit/identity/v3/test_identity_provider.py b/openstackclient/tests/unit/identity/v3/test_identity_provider.py index 1a9a799123..480bae596c 100644 --- a/openstackclient/tests/unit/identity/v3/test_identity_provider.py +++ b/openstackclient/tests/unit/identity/v3/test_identity_provider.py @@ -15,9 +15,12 @@ import copy from unittest import mock +from osc_lib import exceptions + from openstackclient.identity.v3 import identity_provider from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes +from openstackclient.tests.unit import utils as test_utils class TestIdentityProvider(identity_fakes.TestFederatedIdentity): @@ -308,6 +311,86 @@ def test_create_identity_provider_domain_id(self): self.assertEqual(self.columns, columns) self.assertCountEqual(self.datalist, data) + def test_create_identity_provider_authttl_positive(self): + arglist = [ + '--authorization-ttl', '60', + identity_fakes.idp_id, + ] + verifylist = [ + ('identity_provider_id', identity_fakes.idp_id), + ('authorization_ttl', 60), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'remote_ids': None, + 'description': None, + 'domain_id': None, + 'enabled': True, + 'authorization_ttl': 60, + } + + self.identity_providers_mock.create.assert_called_with( + id=identity_fakes.idp_id, + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.datalist, data) + + def test_create_identity_provider_authttl_zero(self): + arglist = [ + '--authorization-ttl', '0', + identity_fakes.idp_id, + ] + verifylist = [ + ('identity_provider_id', identity_fakes.idp_id), + ('authorization_ttl', 0), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'remote_ids': None, + 'description': None, + 'domain_id': None, + 'enabled': True, + 'authorization_ttl': 0, + } + + self.identity_providers_mock.create.assert_called_with( + id=identity_fakes.idp_id, + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.datalist, data) + + def test_create_identity_provider_authttl_negative(self): + arglist = [ + '--authorization-ttl', '-60', + identity_fakes.idp_id, + ] + verifylist = [ + ('identity_provider_id', identity_fakes.idp_id), + ('authorization_ttl', -60), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + def test_create_identity_provider_authttl_not_int(self): + arglist = [ + '--authorization-ttl', 'spam', + identity_fakes.idp_id, + ] + verifylist = [] + self.assertRaises(test_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + class TestIdentityProviderDelete(TestIdentityProvider): @@ -678,6 +761,93 @@ def prepare(self): self.cmd.take_action(parsed_args) + def test_identity_provider_set_authttl_positive(self): + def prepare(self): + """Prepare fake return objects before the test is executed""" + updated_idp = copy.deepcopy(identity_fakes.IDENTITY_PROVIDER) + updated_idp['authorization_ttl'] = 60 + resources = fakes.FakeResource( + None, + updated_idp, + loaded=True + ) + self.identity_providers_mock.update.return_value = resources + + prepare(self) + arglist = [ + '--authorization-ttl', '60', + identity_fakes.idp_id + ] + verifylist = [ + ('identity_provider', identity_fakes.idp_id), + ('enable', False), + ('disable', False), + ('remote_id', None), + ('authorization_ttl', 60), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.identity_providers_mock.update.assert_called_with( + identity_fakes.idp_id, + authorization_ttl=60, + ) + + def test_identity_provider_set_authttl_zero(self): + def prepare(self): + """Prepare fake return objects before the test is executed""" + updated_idp = copy.deepcopy(identity_fakes.IDENTITY_PROVIDER) + updated_idp['authorization_ttl'] = 0 + resources = fakes.FakeResource( + None, + updated_idp, + loaded=True + ) + self.identity_providers_mock.update.return_value = resources + + prepare(self) + arglist = [ + '--authorization-ttl', '0', + identity_fakes.idp_id + ] + verifylist = [ + ('identity_provider', identity_fakes.idp_id), + ('enable', False), + ('disable', False), + ('remote_id', None), + ('authorization_ttl', 0), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.identity_providers_mock.update.assert_called_with( + identity_fakes.idp_id, + authorization_ttl=0, + ) + + def test_identity_provider_set_authttl_negative(self): + arglist = [ + '--authorization-ttl', '-1', + identity_fakes.idp_id + ] + verifylist = [ + ('identity_provider', identity_fakes.idp_id), + ('enable', False), + ('disable', False), + ('remote_id', None), + ('authorization_ttl', -1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + def test_identity_provider_set_authttl_not_int(self): + arglist = [ + '--authorization-ttl', 'spam', + identity_fakes.idp_id + ] + verifylist = [] + self.assertRaises(test_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + class TestIdentityProviderShow(TestIdentityProvider): diff --git a/releasenotes/notes/idp-auth-ttl-6632df5db65a3bdd.yaml b/releasenotes/notes/idp-auth-ttl-6632df5db65a3bdd.yaml new file mode 100644 index 0000000000..dfe19c7ad0 --- /dev/null +++ b/releasenotes/notes/idp-auth-ttl-6632df5db65a3bdd.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + ``identity provider create`` and ``identity provider set`` commands now + accept the ``--authorization-ttl `` argument, with ```` + being a non-negative integer. + + See `note `_ + in Keystone documentations for more details on the meaning of this option. From 62c52f5e61c009ad45fa3e8aeb049821d0b228eb Mon Sep 17 00:00:00 2001 From: ryanKor Date: Sat, 9 Oct 2021 12:05:43 +0900 Subject: [PATCH 006/595] config: Also mask non-prefix config The 'config show' command will show information about your current configuration. When using a 'cloud.yaml' file and the 'OS_CLOUD' environment variable, the output of this will look like so: $ openstack config show +---------------------------------------------+----------------------------------+ | Field | Value | +---------------------------------------------+----------------------------------+ | additional_user_agent | [('osc-lib', '2.6.0')] | | api_timeout | None | | auth.auth_url | https://example.com:13000 | | auth.password | | | auth.project_domain_id | default | | auth.project_id | c73b7097d07c46f78eb4b4dcfbac5ca8 | | auth.project_name | test-project | | auth.user_domain_name | example.com | | auth.username | john-doe | ... All of the 'auth.'-prefixed values are extracted from the corresponding entry in the 'clouds.yaml' file. You'll note that the 'auth.password' value is not shown. Instead, it is masked and replaced with ''. However, a 'clouds.yaml' file is not the only way to configure these tools. You can also use old school environment variables. By using an openrc file from Horizon (or the clouds2env tool [1]), we will set various 'OS_'-prefixed environment variables. When you use the 'config show' command with these environment variables set, we will see all of these values appear in the output *without* an 'auth.' prefix. Scanning down we will see the password value is not redacted. $ openstack config show +---------------------------------------------+----------------------------------+ | Field | Value | +---------------------------------------------+----------------------------------+ | additional_user_agent | [('osc-lib', '2.6.0')] | | api_timeout | None | ... | password | secret-password | ... This will also happen if using tokens. This is obviously incorrect. These should be masked also. Make it so. This involves enhancing our fake config generation code to generate config that looks like it came from environment variables. Change-Id: I560b928e5e6bcdcd89c409e0678dfc0d0b056c0e Story: 2008816 Task: 42260 --- openstackclient/common/configuration.py | 6 +- .../tests/unit/common/test_configuration.py | 57 ++++++++++++++++--- openstackclient/tests/unit/fakes.py | 40 ++++++------- 3 files changed, 71 insertions(+), 32 deletions(-) diff --git a/openstackclient/common/configuration.py b/openstackclient/common/configuration.py index 49ef0e05ca..cb4155059b 100644 --- a/openstackclient/common/configuration.py +++ b/openstackclient/common/configuration.py @@ -45,7 +45,6 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - info = self.app.client_manager.get_configuration() # Assume a default secret list in case we do not have an auth_plugin @@ -63,4 +62,9 @@ def take_action(self, parsed_args): value = REDACTED info['auth.' + key] = value + if parsed_args.mask: + for secret_opt in secret_opts: + if secret_opt in info: + info[secret_opt] = REDACTED + return zip(*sorted(info.items())) diff --git a/openstackclient/tests/unit/common/test_configuration.py b/openstackclient/tests/unit/common/test_configuration.py index bdd3debff5..148228ec08 100644 --- a/openstackclient/tests/unit/common/test_configuration.py +++ b/openstackclient/tests/unit/common/test_configuration.py @@ -35,11 +35,14 @@ class TestConfiguration(utils.TestCommand): fakes.REGION_NAME, ) - opts = [mock.Mock(secret=True, dest="password"), - mock.Mock(secret=True, dest="token")] + opts = [ + mock.Mock(secret=True, dest="password"), + mock.Mock(secret=True, dest="token"), + ] - @mock.patch("keystoneauth1.loading.base.get_plugin_options", - return_value=opts) + @mock.patch( + "keystoneauth1.loading.base.get_plugin_options", return_value=opts + ) def test_show(self, m_get_plugin_opts): arglist = [] verifylist = [('mask', True)] @@ -51,12 +54,14 @@ def test_show(self, m_get_plugin_opts): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) - @mock.patch("keystoneauth1.loading.base.get_plugin_options", - return_value=opts) + @mock.patch( + "keystoneauth1.loading.base.get_plugin_options", return_value=opts + ) def test_show_unmask(self, m_get_plugin_opts): arglist = ['--unmask'] verifylist = [('mask', False)] cmd = configuration.ShowConfiguration(self.app, None) + parsed_args = self.check_parser(cmd, arglist, verifylist) columns, data = cmd.take_action(parsed_args) @@ -71,15 +76,49 @@ def test_show_unmask(self, m_get_plugin_opts): ) self.assertEqual(datalist, data) - @mock.patch("keystoneauth1.loading.base.get_plugin_options", - return_value=opts) - def test_show_mask(self, m_get_plugin_opts): + @mock.patch( + "keystoneauth1.loading.base.get_plugin_options", return_value=opts + ) + def test_show_mask_with_cloud_config(self, m_get_plugin_opts): arglist = ['--mask'] verifylist = [('mask', True)] + self.app.client_manager.configuration_type = "cloud_config" cmd = configuration.ShowConfiguration(self.app, None) + parsed_args = self.check_parser(cmd, arglist, verifylist) columns, data = cmd.take_action(parsed_args) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) + + @mock.patch( + "keystoneauth1.loading.base.get_plugin_options", return_value=opts + ) + def test_show_mask_with_global_env(self, m_get_plugin_opts): + arglist = ['--mask'] + verifylist = [('mask', True)] + self.app.client_manager.configuration_type = "global_env" + column_list = ( + 'identity_api_version', + 'password', + 'region', + 'token', + 'username', + ) + datalist = ( + fakes.VERSION, + configuration.REDACTED, + fakes.REGION_NAME, + configuration.REDACTED, + fakes.USERNAME, + ) + + cmd = configuration.ShowConfiguration(self.app, None) + + parsed_args = self.check_parser(cmd, arglist, verifylist) + + columns, data = cmd.take_action(parsed_args) + + self.assertEqual(column_list, columns) + self.assertEqual(datalist, data) diff --git a/openstackclient/tests/unit/fakes.py b/openstackclient/tests/unit/fakes.py index 00e0c12925..086c246675 100644 --- a/openstackclient/tests/unit/fakes.py +++ b/openstackclient/tests/unit/fakes.py @@ -11,7 +11,6 @@ # 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 import sys @@ -49,21 +48,6 @@ TEST_VERSIONS = fixture.DiscoveryList(href=AUTH_URL) -def to_unicode_dict(catalog_dict): - """Converts dict to unicode dict - - """ - if isinstance(catalog_dict, dict): - return {to_unicode_dict(key): to_unicode_dict(value) - for key, value in catalog_dict.items()} - elif isinstance(catalog_dict, list): - return [to_unicode_dict(element) for element in catalog_dict] - elif isinstance(catalog_dict, str): - return catalog_dict + u"" - else: - return catalog_dict - - class FakeStdout(object): def __init__(self): @@ -142,18 +126,30 @@ def __init__(self): self.network_endpoint_enabled = True self.compute_endpoint_enabled = True self.volume_endpoint_enabled = True + # The source of configuration. This is either 'cloud_config' (a + # clouds.yaml file) or 'global_env' ('OS_'-prefixed envvars) + self.configuration_type = 'cloud_config' def get_configuration(self): - return { - 'auth': { - 'username': USERNAME, - 'password': PASSWORD, - 'token': AUTH_TOKEN, - }, + + config = { 'region': REGION_NAME, 'identity_api_version': VERSION, } + if self.configuration_type == 'cloud_config': + config['auth'] = { + 'username': USERNAME, + 'password': PASSWORD, + 'token': AUTH_TOKEN, + } + elif self.configuration_type == 'global_env': + config['username'] = USERNAME + config['password'] = PASSWORD + config['token'] = AUTH_TOKEN + + return config + def is_network_endpoint_enabled(self): return self.network_endpoint_enabled From 701dd67d6c303bd9199b3185ecbb3c9b3f55c79b Mon Sep 17 00:00:00 2001 From: Snow Kim Date: Sun, 31 Jul 2022 19:10:10 +0900 Subject: [PATCH 007/595] Fix documents for replaced configuration name Since 4.0, '--os-url' global option is no longer supported. And this option had been replaced to '--os-endpoint' - Refer Change-Id: I1b9fbb96e447889a41b705324725a2ffc8ecfd9f - Refer Docs: doc/source/cli/backwards-incompatible.rs I fixed in documents as below - OPTIONS: '--os-url' to '--os-endpoint' - ENVIRONMENT VARIABLES: 'OS_URL' to 'OS_ENDPOINT' - and relevant sentence Change-Id: Ibb0cddf34375df941678ee07a005d81399ec7195 --- doc/source/cli/authentication.rst | 2 +- doc/source/cli/man/openstack.rst | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/source/cli/authentication.rst b/doc/source/cli/authentication.rst index 2e9148c356..2b3ad5dafc 100644 --- a/doc/source/cli/authentication.rst +++ b/doc/source/cli/authentication.rst @@ -36,7 +36,7 @@ There are at least three authentication types that are always available: URL for the Identity service is also required. [Required: ``--os-auth-url``, ``--os-project-name``, ``--os-username``; Optional: ``--os-password``] * **Token**: This is slightly different from the usual token authentication - (described below as token/endpoint) in that a token and an authentication + in that a token and an authentication URL are supplied and the plugin retrieves a new token. [Required: ``--os-auth-url``, ``--os-token``] * **Others**: Other authentication plugins such as SAML, Kerberos, and OAuth1.0 diff --git a/doc/source/cli/man/openstack.rst b/doc/source/cli/man/openstack.rst index dc327a66e8..29db064199 100644 --- a/doc/source/cli/man/openstack.rst +++ b/doc/source/cli/man/openstack.rst @@ -49,7 +49,7 @@ command line. The primary difference is the use of 'project' in the name of the Refer to the keystoneclient library documentation for more details about these plugins and their options, and for a complete list of available plugins. Please bear in mind that some plugins might not support all of the functionalities of :program:`openstack`; for example the v3unscopedsaml plugin can deliver only unscoped tokens, some commands might not be available through this authentication method. -Additionally, it is possible to use Keystone's service token to authenticate, by setting the options :option:`--os-token` and :option:`--os-url` (or the environment variables :envvar:`OS_TOKEN` and :envvar:`OS_URL` respectively). This method takes precedence over authentication plugins. +Additionally, it is possible to use Keystone's service token to authenticate, by setting the options :option:`--os-token` and :option:`--os-endpoint` (or the environment variables :envvar:`OS_TOKEN` and :envvar:`OS_ENDPOINT` respectively). This method takes precedence over authentication plugins. .. NOTE:: To use the ``v3unscopedsaml`` method, the lxml package will need to be installed. @@ -106,9 +106,9 @@ OPTIONS Authentication URL -.. option:: --os-url +.. option:: --os-endpoint - Service URL, when using a service token for authentication + Service ENDPOINT, when using a service token for authentication .. option:: --os-domain-name @@ -349,7 +349,7 @@ file, but if those are incomplete it may be impossible to know which auth type is intended. The :option:`--os-auth-type` option can always be used to force a specific type. -When :option:`--os-token` and :option:`--os-url` are both present the +When :option:`--os-token` and :option:`--os-endpoint` are both present the ``token_endpoint`` auth type is selected automatically. If :option:`--os-auth-url` and :option:`--os-username` are present ``password`` auth type is selected. @@ -466,9 +466,9 @@ The following environment variables can be set to alter the behaviour of :progra - ``v3applicationcredential`` - ``v3multifactor`` -.. envvar:: OS_URL +.. envvar:: OS_ENDPOINT - Service URL (when using the service token) + Service ENDPOINT (when using the service token) .. envvar:: OS_DOMAIN_NAME From dd648dcb5c952f095b8d6700559c3274b9c1093c Mon Sep 17 00:00:00 2001 From: niuke Date: Mon, 15 Aug 2022 12:19:07 +0800 Subject: [PATCH 008/595] remove unicode prefix from code Change-Id: I0b1ca4237de61b70c694f36e1956e71a2a6b39a4 --- doc/source/conf.py | 16 ++++++++-------- releasenotes/source/conf.py | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 4b60ce4a5d..5eb5f59f69 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -51,8 +51,8 @@ master_doc = 'index' # General information about the project. -project = u'OpenStack Command Line Client' -copyright = u'2012-2013 OpenStack Foundation' +project = 'OpenStack Command Line Client' +copyright = '2012-2013 OpenStack Foundation' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -189,8 +189,8 @@ # . latex_documents = [ ('index', 'OpenStackCommandLineClient.tex', - u'OpenStack Command Line Client Documentation', - u'OpenStack', 'manual'), + 'OpenStack Command Line Client Documentation', + 'OpenStack', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -222,8 +222,8 @@ ( 'cli/man/openstack', 'openstack', - u'OpenStack Command Line Client', - [u'OpenStack contributors'], + 'OpenStack Command Line Client', + ['OpenStack contributors'], 1, ), ] @@ -239,8 +239,8 @@ # dir menu entry, description, category) texinfo_documents = [ ('index', 'OpenStackCommandLineClient', - u'OpenStack Command Line Client Documentation', - u'OpenStack', 'OpenStackCommandLineClient', + 'OpenStack Command Line Client Documentation', + 'OpenStack', 'OpenStackCommandLineClient', 'One line description of project.', 'Miscellaneous'), ] diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index 179f8f23fc..35f061afa2 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -88,8 +88,8 @@ master_doc = 'index' # General information about the project. -project = u'OpenStackClient Release Notes' -copyright = u'2015, OpenStackClient Developers' +project = 'OpenStackClient Release Notes' +copyright = '2015, OpenStackClient Developers' # Release notes are version independent. # The full version, including alpha/beta/rc tags. @@ -238,8 +238,8 @@ latex_documents = [( 'index', 'OpenStackClientReleaseNotes.tex', - u'OpenStackClient Release Notes Documentation', - u'OpenStackClient Developers', + 'OpenStackClient Release Notes Documentation', + 'OpenStackClient Developers', 'manual', )] @@ -271,8 +271,8 @@ man_pages = [( 'index', 'openstackclientreleasenotes', - u'OpenStackClient Release Notes Documentation', - [u'OpenStackClient Developers'], + 'OpenStackClient Release Notes Documentation', + ['OpenStackClient Developers'], 1, )] @@ -288,8 +288,8 @@ texinfo_documents = [( 'index', 'OpenStackClientReleaseNotes', - u'OpenStackclient Release Notes Documentation', - u'OpenStackclient Developers', + 'OpenStackclient Release Notes Documentation', + 'OpenStackclient Developers', 'OpenStackClientReleaseNotes', 'A unified command-line client for OpenStack.', 'Miscellaneous', From 6e0699c1cf3e603850580a6bb5ca2e377fe1bc84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Ribaud?= Date: Thu, 3 Mar 2022 17:50:08 +0100 Subject: [PATCH 009/595] Microversion 2.91: Support specifying destination host to unshelve This patch adds a new parameter ``--host`` to ``openstack server unshelve`` command. This can help administrators to specify an ``host`` to unshelve a shelve offloaded server. And add new parameter ``--no-availability-zone`` to unpin a server availability These parameters are available in the 2.91 microversion. Depends-On: https://review.opendev.org/c/openstack/python-novaclient/+/831651 Implements: blueprint unshelve-to-host Change-Id: I7986adc7563f63bcd4b3caf5eb7bc4329b4e1eca --- openstackclient/compute/v2/server.py | 39 +++- .../tests/unit/compute/v2/test_server.py | 179 +++++++++++++++++- .../bp-unshelve-to-host-9ce4b7abf81aeedf.yaml | 7 + 3 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/bp-unshelve-to-host-9ce4b7abf81aeedf.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index b02ee6ff7f..0bcad8fc3c 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -4564,13 +4564,30 @@ def get_parser(self, prog_name): nargs='+', help=_('Server(s) to unshelve (name or ID)'), ) - parser.add_argument( + group = parser.add_mutually_exclusive_group() + group.add_argument( '--availability-zone', default=None, help=_('Name of the availability zone in which to unshelve a ' 'SHELVED_OFFLOADED server (supported by ' '--os-compute-api-version 2.77 or above)'), ) + group.add_argument( + '--no-availability-zone', + action='store_true', + default=False, + help=_('Unpin the availability zone of a SHELVED_OFFLOADED ' + 'server. Server will be unshelved on a host without ' + 'availability zone constraint (supported by ' + '--os-compute-api-version 2.91 or above)'), + ) + parser.add_argument( + '--host', + default=None, + help=_('Name of the destination host in which to unshelve a ' + 'SHELVED_OFFLOADED server (supported by ' + '--os-compute-api-version 2.91 or above)'), + ) parser.add_argument( '--wait', action='store_true', @@ -4599,6 +4616,26 @@ def _show_progress(progress): kwargs['availability_zone'] = parsed_args.availability_zone + if parsed_args.host: + if compute_client.api_version < api_versions.APIVersion('2.91'): + msg = _( + '--os-compute-api-version 2.91 or greater is required ' + 'to support the --host option' + ) + raise exceptions.CommandError(msg) + + kwargs['host'] = parsed_args.host + + if parsed_args.no_availability_zone: + if compute_client.api_version < api_versions.APIVersion('2.91'): + msg = _( + '--os-compute-api-version 2.91 or greater is required ' + 'to support the --no-availability-zone option' + ) + raise exceptions.CommandError(msg) + + kwargs['availability_zone'] = None + for server in parsed_args.server: server_obj = utils.find_resource( compute_client.servers, diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 004f3a0531..ba86dff33f 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -161,6 +161,10 @@ def setup_sdk_volumes_mock(self, count): return volumes def run_method_with_servers(self, method_name, server_count): + # Starting with v2.91, the nova api needs to be call with a sentinel + # as availability_zone=None will unpin the server az. + _sentinel = object() + servers = self.setup_servers_mock(server_count) arglist = [] @@ -183,7 +187,11 @@ def run_method_with_servers(self, method_name, server_count): method.assert_called_with(reason=None) elif method_name == 'unshelve': version = self.app.client_manager.compute.api_version - if version >= api_versions.APIVersion('2.77'): + if version >= api_versions.APIVersion('2.91'): + method.assert_called_with(availability_zone=_sentinel, + host=None) + elif (version >= api_versions.APIVersion('2.77') and + version < api_versions.APIVersion('2.91')): method.assert_called_with(availability_zone=None) else: method.assert_called_with() @@ -8204,7 +8212,23 @@ def test_unshelve_one_server(self): def test_unshelve_multi_servers(self): self.run_method_with_servers('unshelve', 3) - def test_unshelve_with_specified_az(self): + def test_unshelve_v277(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.77') + + server = compute_fakes.FakeServer.create_one_server( + attrs=self.attrs, methods=self.methods) + self.servers_mock.get.return_value = server + arglist = [server.id] + verifylist = [('server', [server.id])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(server.id) + server.unshelve.assert_called_with() + + def test_unshelve_with_specified_az_v277(self): self.app.client_manager.compute.api_version = \ api_versions.APIVersion('2.77') @@ -8248,6 +8272,157 @@ def test_unshelve_with_specified_az_pre_v277(self): self.assertIn( '--os-compute-api-version 2.77 or greater is required', str(ex)) + def test_unshelve_v291(self): + self.app.client_manager.compute.api_version = ( + api_versions.APIVersion('2.91')) + + server = compute_fakes.FakeServer.create_one_server( + attrs=self.attrs, methods=self.methods) + self.servers_mock.get.return_value = server + arglist = [server.id] + verifylist = [('server', [server.id])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(server.id) + server.unshelve.assert_called_with() + + def test_unshelve_with_specified_az_v291(self): + self.app.client_manager.compute.api_version = ( + api_versions.APIVersion('2.91')) + + server = compute_fakes.FakeServer.create_one_server( + attrs=self.attrs, methods=self.methods) + self.servers_mock.get.return_value = server + arglist = [ + '--availability-zone', "foo-az", + server.id, + ] + verifylist = [ + ('availability_zone', "foo-az"), + ('server', [server.id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(server.id) + server.unshelve.assert_called_with(availability_zone="foo-az") + + def test_unshelve_with_specified_host_v291(self): + self.app.client_manager.compute.api_version = ( + api_versions.APIVersion('2.91')) + + server = compute_fakes.FakeServer.create_one_server( + attrs=self.attrs, methods=self.methods) + self.servers_mock.get.return_value = server + arglist = [ + '--host', "server1", + server.id, + ] + verifylist = [ + ('host', "server1"), + ('server', [server.id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(server.id) + server.unshelve.assert_called_with(host="server1") + + def test_unshelve_with_unpin_az_v291(self): + self.app.client_manager.compute.api_version = ( + api_versions.APIVersion('2.91')) + + server = compute_fakes.FakeServer.create_one_server( + attrs=self.attrs, methods=self.methods) + self.servers_mock.get.return_value = server + arglist = ['--no-availability-zone', server.id] + verifylist = [ + ('no_availability_zone', True), + ('server', [server.id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(server.id) + server.unshelve.assert_called_with(availability_zone=None) + + def test_unshelve_with_specified_az_and_host_v291(self): + self.app.client_manager.compute.api_version = ( + api_versions.APIVersion('2.91')) + + server = compute_fakes.FakeServer.create_one_server( + attrs=self.attrs, methods=self.methods) + self.servers_mock.get.return_value = server + arglist = [ + '--host', "server1", + '--availability-zone', "foo-az", + server.id, + ] + verifylist = [ + ('host', "server1"), + ('availability_zone', "foo-az"), + ('server', [server.id]) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(server.id) + + def test_unshelve_with_unpin_az_and_host_v291(self): + self.app.client_manager.compute.api_version = ( + api_versions.APIVersion('2.91')) + + server = compute_fakes.FakeServer.create_one_server( + attrs=self.attrs, methods=self.methods) + self.servers_mock.get.return_value = server + arglist = [ + '--host', "server1", + '--no-availability-zone', + server.id, + ] + verifylist = [ + ('host', "server1"), + ('no_availability_zone', True), + ('server', [server.id]) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(server.id) + + def test_unshelve_fails_with_unpin_az_and_az_v291(self): + self.app.client_manager.compute.api_version = ( + api_versions.APIVersion('2.91')) + + server = compute_fakes.FakeServer.create_one_server( + attrs=self.attrs, methods=self.methods) + self.servers_mock.get.return_value = server + arglist = [ + '--availability-zone', "foo-az", + '--no-availability-zone', + server.id, + ] + verifylist = [ + ('availability_zone', "foo-az"), + ('no_availability_zone', True), + ('server', [server.id]) + ] + + ex = self.assertRaises(utils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + self.assertIn('argument --no-availability-zone: not allowed ' + 'with argument --availability-zone', str(ex)) + @mock.patch.object(common_utils, 'wait_for_status', return_value=True) def test_unshelve_with_wait(self, mock_wait_for_status): server = compute_fakes.FakeServer.create_one_server( diff --git a/releasenotes/notes/bp-unshelve-to-host-9ce4b7abf81aeedf.yaml b/releasenotes/notes/bp-unshelve-to-host-9ce4b7abf81aeedf.yaml new file mode 100644 index 0000000000..54a31d63fb --- /dev/null +++ b/releasenotes/notes/bp-unshelve-to-host-9ce4b7abf81aeedf.yaml @@ -0,0 +1,7 @@ +--- +features: + - Add ``--host`` and ``--no-availability-zone`` options to the + ``server unshelve`` command to enable administrators to specify a + destination host or unset the availability zone during a server + unshelve, respectively. Both options require the server to be + shelved offload and ``--os-compute-api-version 2.91`` or greater. From 84003aa6fe6aa1a570ced518bca473f1e5159cba Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 24 Aug 2022 11:00:57 +0100 Subject: [PATCH 010/595] Bump python-novaclient version The recently added support for nova API microversions 2.91 and 2.92 necessitate a newer version of python-novaclient. Make it so. Change-Id: I40a77334e4aee2e196062c38a0bb70f8f3079b44 Signed-off-by: Stephen Finucane --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 118b7b95d3..8cc84809de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,6 @@ osc-lib>=2.3.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 python-keystoneclient>=3.22.0 # Apache-2.0 -python-novaclient>=17.0.0 # Apache-2.0 +python-novaclient>=18.1.0 # Apache-2.0 python-cinderclient>=3.3.0 # Apache-2.0 stevedore>=2.0.1 # Apache-2.0 From 8b0e467355440779e6463b88f7e6689110b9db1a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 24 Aug 2022 11:02:39 +0100 Subject: [PATCH 011/595] Remove invalid note from requirements files This isn't true with the new dependency resolver introduced in pip 20.3. Change-Id: Ie28fe625663c960fbffa12efb1503b8c9f8c0581 Signed-off-by: Stephen Finucane --- doc/requirements.txt | 3 --- requirements.txt | 3 --- test-requirements.txt | 3 --- 3 files changed, 9 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 60a877970c..8b4202be8e 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,6 +1,3 @@ -# The order of packages is significant, because pip processes them in the order -# of appearance. Changing the order has an impact on the overall integration -# process, which may cause wedges in the gate later. openstackdocstheme>=2.2.1 # Apache-2.0 reno>=3.1.0 # Apache-2.0 sphinx>=2.0.0,!=2.1.0 # BSD diff --git a/requirements.txt b/requirements.txt index 8cc84809de..d14c324610 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,9 +2,6 @@ # date but we do not test them so no guarantee of having them all correct. If # you find any incorrect lower bounds, let us know or propose a fix. -# The order of packages is significant, because pip processes them in the order -# of appearance. Changing the order has an impact on the overall integration -# process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 cliff>=3.5.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 8b61a5c077..275c2ff114 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,6 +1,3 @@ -# The order of packages is significant, because pip processes them in the order -# of appearance. Changing the order has an impact on the overall integration -# process, which may cause wedges in the gate later. coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD oslotest>=3.2.0 # Apache-2.0 From 2093a5b68192714c9a4396137c3c8ee87ade124c Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Thu, 25 Aug 2022 09:11:12 +0900 Subject: [PATCH 012/595] Fix missing closing brackets in metavar ... to display the complete format in --help output. Change-Id: I4d0f044072b206f6205b4b4f4992fd08f01729d6 --- openstackclient/network/v2/network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index d7b37ebede..094432f4da 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -665,12 +665,12 @@ def get_parser(self, prog_name): ) parser.add_argument( '--description', - metavar=" Date: Tue, 15 Mar 2022 15:49:12 +0100 Subject: [PATCH 013/595] network: Add tenant project filter for RBAC list Implements a new parser argument "--target-project" to list RBAC policies for a specific tenant project only. This uses the already existing server-side query parameter "target_tenant". Story: 2009937 Task: 44824 Depends-On: https://review.opendev.org/c/openstack/openstacksdk/+/834442 Change-Id: I83ff07041a022e8795e3c5550c6a7aabb0c0d8c8 Signed-off-by: Jan Hartkopf --- openstackclient/network/v2/network_rbac.py | 15 +++++++++++++++ .../unit/network/v2/test_network_rbac.py | 19 +++++++++++++++++++ ...enant-project-filter-1228f2287284e33c.yaml | 5 +++++ 3 files changed, 39 insertions(+) create mode 100644 releasenotes/notes/add-network-rbac-list-tenant-project-filter-1228f2287284e33c.yaml diff --git a/openstackclient/network/v2/network_rbac.py b/openstackclient/network/v2/network_rbac.py index 91cd9dc32d..fa4fca7c40 100644 --- a/openstackclient/network/v2/network_rbac.py +++ b/openstackclient/network/v2/network_rbac.py @@ -217,6 +217,11 @@ def get_parser(self, prog_name): help=_('List network RBAC policies according to given ' 'action ("access_as_external" or "access_as_shared")') ) + parser.add_argument( + '--target-project', + metavar='', + help=_('List network RBAC policies for a specific target project') + ) parser.add_argument( '--long', action='store_true', @@ -247,6 +252,16 @@ def take_action(self, parsed_args): query['object_type'] = parsed_args.type if parsed_args.action is not None: query['action'] = parsed_args.action + if parsed_args.target_project is not None: + project_id = "*" + + if parsed_args.target_project != "*": + identity_client = self.app.client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.target_project, + ).id + query['target_project_id'] = project_id data = client.rbac_policies(**query) diff --git a/openstackclient/tests/unit/network/v2/test_network_rbac.py b/openstackclient/tests/unit/network/v2/test_network_rbac.py index b0bc7e86cb..7ce252054d 100644 --- a/openstackclient/tests/unit/network/v2/test_network_rbac.py +++ b/openstackclient/tests/unit/network/v2/test_network_rbac.py @@ -405,6 +405,9 @@ def setUp(self): self.network.rbac_policies = mock.Mock(return_value=self.rbac_policies) + self.project = identity_fakes_v3.FakeProject.create_one_project() + self.projects_mock.get.return_value = self.project + def test_network_rbac_list(self): arglist = [] verifylist = [] @@ -466,6 +469,22 @@ def test_network_rbac_list_with_long(self): self.assertEqual(self.columns_long, columns) self.assertEqual(self.data_long, list(data)) + def test_network_rbac_list_target_project_opt(self): + arglist = [ + '--target-project', self.rbac_policies[0].target_project_id, ] + verifylist = [ + ('target_project', self.rbac_policies[0].target_project_id)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.network.rbac_policies.assert_called_with(**{ + 'target_project_id': self.project.id + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + class TestSetNetworkRBAC(TestNetworkRBAC): diff --git a/releasenotes/notes/add-network-rbac-list-tenant-project-filter-1228f2287284e33c.yaml b/releasenotes/notes/add-network-rbac-list-tenant-project-filter-1228f2287284e33c.yaml new file mode 100644 index 0000000000..82fe6aaa76 --- /dev/null +++ b/releasenotes/notes/add-network-rbac-list-tenant-project-filter-1228f2287284e33c.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add a new argument ``--target-project`` to the ``network rbac list`` + command to filter for a specific target project. \ No newline at end of file From c6065c7a4796ad0a679a13cb6bd2b3b5a271f5cb Mon Sep 17 00:00:00 2001 From: elajkat Date: Thu, 8 Sep 2022 14:43:52 +0200 Subject: [PATCH 014/595] Add address-scope to NDP proxy tests Related-Bug: #1987410 Change-Id: I61df81381803ca289f295ab8d7a8f495bb169447 --- .../network/v2/test_network_ndp_proxy.py | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/openstackclient/tests/functional/network/v2/test_network_ndp_proxy.py b/openstackclient/tests/functional/network/v2/test_network_ndp_proxy.py index e89197ad33..a10aef6b55 100644 --- a/openstackclient/tests/functional/network/v2/test_network_ndp_proxy.py +++ b/openstackclient/tests/functional/network/v2/test_network_ndp_proxy.py @@ -32,8 +32,26 @@ def setUp(self): self.INT_NET_NAME = self.getUniqueString() self.INT_SUB_NAME = self.getUniqueString() self.INT_PORT_NAME = self.getUniqueString() + self.ADDR_SCOPE_NAME = self.getUniqueString() + self.SUBNET_P_NAME = self.getUniqueString() self.created_ndp_proxies = [] + json_output = json.loads( + self.openstack( + 'address scope create -f json --ip-version 6 ' + '%(address_s_name)s' % { + 'address_s_name': self.ADDR_SCOPE_NAME})) + self.assertIsNotNone(json_output['id']) + self.ADDRESS_SCOPE_ID = json_output['id'] + json_output = json.loads( + self.openstack( + 'subnet pool create -f json %(subnet_p_name)s ' + '--address-scope %(address_scope)s ' + '--pool-prefix 2001:db8::/96 --default-prefix-length 112' % { + 'subnet_p_name': self.SUBNET_P_NAME, + 'address_scope': self.ADDRESS_SCOPE_ID})) + self.assertIsNotNone(json_output['id']) + self.SUBNET_POOL_ID = json_output['id'] json_output = json.loads( self.openstack('network create -f json ' '--external ' + self.EXT_NET_NAME)) @@ -41,8 +59,9 @@ def setUp(self): self.EXT_NET_ID = json_output['id'] json_output = json.loads( self.openstack( - 'subnet create -f json --ip-version 6 --subnet-range ' - '2002::1:0/112 --network %(net_id)s %(sub_name)s' % { + 'subnet create -f json --ip-version 6 --subnet-pool ' + '%(subnet_pool)s --network %(net_id)s %(sub_name)s' % { + 'subnet_pool': self.SUBNET_POOL_ID, 'net_id': self.EXT_NET_ID, 'sub_name': self.EXT_SUB_NAME})) self.assertIsNotNone(json_output['id']) @@ -68,8 +87,9 @@ def setUp(self): self.INT_NET_ID = json_output['id'] json_output = json.loads( self.openstack( - 'subnet create -f json --ip-version 6 --subnet-range ' - '2002::2:0/112 --network %(net_id)s %(sub_name)s' % { + 'subnet create -f json --ip-version 6 --subnet-pool ' + '%(subnet_pool)s --network %(net_id)s %(sub_name)s' % { + 'subnet_pool': self.SUBNET_POOL_ID, 'net_id': self.INT_NET_ID, 'sub_name': self.INT_SUB_NAME})) self.assertIsNotNone(json_output['id']) @@ -113,6 +133,11 @@ def tearDown(self): self.assertEqual('', output) output = self.openstack('network delete ' + self.EXT_NET_ID) self.assertEqual('', output) + output = self.openstack('subnet pool delete ' + self.SUBNET_POOL_ID) + self.assertEqual('', output) + output = self.openstack('address scope delete ' + + self.ADDRESS_SCOPE_ID) + self.assertEqual('', output) super().tearDown() def _create_ndp_proxies(self, ndp_proxies): From 1f63034441a63b968185e1678049c01205b8e6d7 Mon Sep 17 00:00:00 2001 From: whoami-rajat Date: Wed, 14 Sep 2022 11:01:31 +0100 Subject: [PATCH 015/595] compute: Require image when rebuilding a volume-backed server A volume-backed server will have no image attribute (or rather the image property will be set to the empty string). As such, if you want to try rebuild you will need to specify an image [*]. Enforce this. [*] Before microversion 2.93, this must be the same image. However, we don't touch on that here. This will be addressed later. Change-Id: I6842dabd7acb4e3a78f894e55e616625757eb6a4 Story: 2010297 Task: 46290 --- openstackclient/compute/v2/server.py | 14 +++++++++++--- .../tests/unit/compute/v2/test_server.py | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 0bcad8fc3c..24c158ae93 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -3111,13 +3111,21 @@ def _show_progress(progress): server = utils.find_resource( compute_client.servers, parsed_args.server) - # If parsed_args.image is not set, default to the currently used one. + # If parsed_args.image is not set and if the instance is image backed, + # default to the currently used one. If the instance is volume backed, + # it is not trivial to fetch the current image and probably better + # to error out in this case and ask user to supply the image. if parsed_args.image: image = image_client.find_image( parsed_args.image, ignore_missing=False) else: - image_id = server.to_dict().get('image', {}).get('id') - image = image_client.get_image(image_id) + if not server.image: + msg = _( + 'The --image option is required when rebuilding a ' + 'volume-backed server' + ) + raise exceptions.CommandError(msg) + image = image_client.get_image(server.image['id']) kwargs = {} diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index ba86dff33f..3daa4cc75e 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -5781,6 +5781,25 @@ def test_rebuild_with_current_image(self): self.get_image_mock.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(self.image, None) + def test_rebuild_with_volume_backed_server_no_image(self): + # the volume-backed server will have the image attribute set to an + # empty string, not null/None + self.server.image = '' + + arglist = [ + self.server.id, + ] + verifylist = [ + ('server', self.server.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn('The --image option is required', str(exc)) + def test_rebuild_with_name(self): name = 'test-server-xxx' arglist = [ From 4024bdb3933dd79eec4bcf99c13f3dbf17add40b Mon Sep 17 00:00:00 2001 From: whoami-rajat Date: Fri, 25 Feb 2022 22:43:24 +0530 Subject: [PATCH 016/595] compute: Add support for microversion 2.93 Add '--reimage-boot-volume' and '--no-reimage-boot-volume parameters' to the rebuild command to allow rebuilding of volume backed instances. Change-Id: I4a6e30b2cf12f32202a2d9ef1ced347e1dd139f3 --- openstackclient/compute/v2/server.py | 57 +++++++++++ .../tests/unit/compute/v2/test_server.py | 97 +++++++++++++++++++ ...age-param-to-rebuild-606dd331677b5954.yaml | 8 ++ 3 files changed, 162 insertions(+) create mode 100644 releasenotes/notes/add-reimage-param-to-rebuild-606dd331677b5954.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 24c158ae93..02fed1550e 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -3091,6 +3091,28 @@ def get_parser(self, prog_name): '(supported by --os-compute-api-version 2.90 or above)' ), ) + parser.add_argument( + '--reimage-boot-volume', + action='store_true', + dest='reimage_boot_volume', + default=None, + help=_( + 'Rebuild a volume-backed server. This will wipe the root ' + 'volume data and overwrite it with the provided image. ' + 'Defaults to False. ' + '(supported by --os-compute-api-version 2.93 or above)' + ), + ) + parser.add_argument( + '--no-reimage-boot-volume', + action='store_false', + dest='reimage_boot_volume', + default=None, + help=_( + 'Do not rebuild a volume-backed server. ' + '(supported by --os-compute-api-version 2.93 or above)' + ), + ) parser.add_argument( '--wait', action='store_true', @@ -3226,6 +3248,41 @@ def _show_progress(progress): kwargs['hostname'] = parsed_args.hostname + v2_93 = api_versions.APIVersion('2.93') + if parsed_args.reimage_boot_volume: + if compute_client.api_version < v2_93: + msg = _( + '--os-compute-api-version 2.93 or greater is required to ' + 'support the --reimage-boot-volume option' + ) + raise exceptions.CommandError(msg) + else: + # force user to explicitly request reimaging of volume-backed + # server + if not server.image: + if compute_client.api_version >= v2_93: + msg = ( + '--reimage-boot-volume is required to rebuild a ' + 'volume-backed server' + ) + raise exceptions.CommandError(msg) + else: # microversion < 2.93 + # attempts to rebuild a volume-backed server before API + # microversion 2.93 will fail in all cases except one: if + # the user attempts the rebuild with the exact same image + # that the server was initially built with. We can't check + # for this since we don't have the original image ID to + # hand, so we simply warn the user. + # TODO(stephenfin): Make this a failure in a future + # version + self.log.warning( + 'Attempting to rebuild a volume-backed server using ' + '--os-compute-api-version 2.92 or earlier, which ' + 'will only succeed if the image is identical to the ' + 'one initially used. This will be an error in a ' + 'future release.' + ) + try: server = server.rebuild(image, parsed_args.password, **kwargs) finally: diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 3daa4cc75e..d393acdaf1 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -6294,6 +6294,103 @@ def test_rebuild_with_hostname_pre_v290(self): parsed_args) +class TestServerRebuildVolumeBacked(TestServer): + + def setUp(self): + super().setUp() + + self.new_image = image_fakes.create_one_image() + self.find_image_mock.return_value = self.new_image + + attrs = { + 'image': '', + 'networks': {}, + 'adminPass': 'passw0rd', + } + new_server = compute_fakes.FakeServer.create_one_server(attrs=attrs) + + # Fake the server to be rebuilt. The IDs of them should be the same. + attrs['id'] = new_server.id + methods = { + 'rebuild': new_server, + } + self.server = compute_fakes.FakeServer.create_one_server( + attrs=attrs, + methods=methods + ) + + # Return value for utils.find_resource for server. + self.servers_mock.get.return_value = self.server + + self.cmd = server.RebuildServer(self.app, None) + + def test_rebuild_with_reimage_boot_volume(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.93') + + arglist = [ + self.server.id, + '--reimage-boot-volume', + '--image', self.new_image.id + ] + verifylist = [ + ('server', self.server.id), + ('reimage_boot_volume', True), + ('image', self.new_image.id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server.rebuild.assert_called_with( + self.new_image, None) + + def test_rebuild_with_no_reimage_boot_volume(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.93') + + arglist = [ + self.server.id, + '--no-reimage-boot-volume', + '--image', self.new_image.id + ] + verifylist = [ + ('server', self.server.id), + ('reimage_boot_volume', False), + ('image', self.new_image.id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn('--reimage-boot-volume is required', str(exc)) + + def test_rebuild_with_reimage_boot_volume_pre_v293(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.92') + + arglist = [ + self.server.id, + '--reimage-boot-volume', + '--image', self.new_image.id + ] + verifylist = [ + ('server', self.server.id), + ('reimage_boot_volume', True) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.93 or greater is required', str(exc)) + + class TestEvacuateServer(TestServer): def setUp(self): diff --git a/releasenotes/notes/add-reimage-param-to-rebuild-606dd331677b5954.yaml b/releasenotes/notes/add-reimage-param-to-rebuild-606dd331677b5954.yaml new file mode 100644 index 0000000000..01a524eb8b --- /dev/null +++ b/releasenotes/notes/add-reimage-param-to-rebuild-606dd331677b5954.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + The ``server rebuild`` commands now accept two optional + ``--reimage-boot-volume`` and ``--no-reimage-boot-volume``option. + Passing these parameter will allow/disallow a user to rebuild a volume + backed server. + This is available from Compute microversion ``2.93`` and onwards. From 3fc585332f79358489285eabc298e01007599128 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 14 Sep 2022 13:40:17 +0100 Subject: [PATCH 017/595] volume: Volume names are optional Who knew? Change-Id: I1c1a811c0db59250e8a20a87f25c2662bc3ab3e8 Signed-off-by: Stephen Finucane --- openstackclient/tests/unit/volume/v2/test_volume.py | 4 +--- openstackclient/volume/v2/volume.py | 3 ++- releasenotes/notes/optional-volume-name-ffbefe463a598b6c.yaml | 4 ++++ 3 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/optional-volume-name-ffbefe463a598b6c.yaml diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index ec82e6746b..b6e9f487f2 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -108,11 +108,9 @@ def setUp(self): def test_volume_create_min_options(self): arglist = [ '--size', str(self.new_volume.size), - self.new_volume.name, ] verifylist = [ ('size', self.new_volume.size), - ('name', self.new_volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -124,7 +122,7 @@ def test_volume_create_min_options(self): self.volumes_mock.create.assert_called_with( size=self.new_volume.size, snapshot_id=None, - name=self.new_volume.name, + name=None, description=None, volume_type=None, availability_zone=None, diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index 6d14b360de..1e1fde9226 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -82,10 +82,11 @@ class CreateVolume(command.ShowOne): _description = _("Create new volume") def get_parser(self, prog_name): - parser = super(CreateVolume, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( "name", metavar="", + nargs="?", help=_("Volume name"), ) parser.add_argument( diff --git a/releasenotes/notes/optional-volume-name-ffbefe463a598b6c.yaml b/releasenotes/notes/optional-volume-name-ffbefe463a598b6c.yaml new file mode 100644 index 0000000000..377d453dee --- /dev/null +++ b/releasenotes/notes/optional-volume-name-ffbefe463a598b6c.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + The ```` argument for the ``volume create`` command is now optional. From a9b9252da3867fe1fec4d162e23545ebc277347c Mon Sep 17 00:00:00 2001 From: zeze Date: Thu, 15 Sep 2022 00:27:42 +0900 Subject: [PATCH 018/595] Adding missing command mapping in docs nova command: version-list openstack command: versions show --service compute cinder command: version-list openstack command: versions show --service block-storage glance command: member-list openstack command: image member list Change-Id: I3c68dbd47dc92ed190ae9abaa90dfcddef072728 --- doc/source/cli/data/cinder.csv | 2 +- doc/source/cli/data/glance.csv | 2 +- doc/source/cli/data/nova.csv | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/cli/data/cinder.csv b/doc/source/cli/data/cinder.csv index eeaaeacce0..3727796692 100644 --- a/doc/source/cli/data/cinder.csv +++ b/doc/source/cli/data/cinder.csv @@ -139,7 +139,7 @@ type-show,volume type show,Show volume type details. type-update,volume type set,"Updates volume type name description and/or is_public." unmanage,volume delete --remote,Stop managing a volume. upload-to-image,image create --volume,Uploads volume to Image Service as an image. -version-list,,List all API versions. (Supported by API versions 3.0 - 3.latest) +version-list,versions show --service block-storage,List all API versions. (Supported by API versions 3.0 - 3.latest) work-cleanup,,Request cleanup of services with optional filtering. (Supported by API versions 3.24 - 3.latest) bash-completion,complete,Prints arguments for bash_completion. help,help,Shows help about this program or one of its subcommands. diff --git a/doc/source/cli/data/glance.csv b/doc/source/cli/data/glance.csv index 27585b9510..0c34c7b953 100644 --- a/doc/source/cli/data/glance.csv +++ b/doc/source/cli/data/glance.csv @@ -49,7 +49,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-list,,Describe sharing permissions by image. +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. stores-info,,Print available backends from Glance. diff --git a/doc/source/cli/data/nova.csv b/doc/source/cli/data/nova.csv index 46459163fc..4252585f9b 100644 --- a/doc/source/cli/data/nova.csv +++ b/doc/source/cli/data/nova.csv @@ -118,7 +118,7 @@ unshelve,server unshelve,Unshelve a server. update,server set / unset,Update the name or the description for a server. usage,usage show,Show usage data for a single tenant. usage-list,usage list,List usage data for all tenants. -version-list,,List all API versions. +version-list,versions show --service compute,List all API versions. volume-attach,server add volume,Attach a volume to a server. volume-attachments,server show,List all the volumes attached to a server. volume-detach,server remove volume,Detach a volume from a server. From 7e5b528e2e1503392f968f75fcf4a7bb0108378d Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Sat, 20 Aug 2022 10:06:28 +0900 Subject: [PATCH 019/595] Replace assertItemsEqual with assertCountEqual The assertItemsEqual were replaced with assertCountEqual in I0bbffbec8889b8b3067cfe17d258f5cb16624f38. However the following changes add assertItemsEqual after that. * I1095100efb27b8559412469f0a9d07fc0a3db9d5 * Ic230c2c5cda8255d8f2c422880aeac81670b2df3 * Ica3320242a38901c1180b2b29109c9474366fde0 So Replace assertItemsEqual with assertCountEqual again. Change-Id: I11ff1748225e434f64dbaf7b88dc80ba28a5e2a0 Signed-off-by: Takashi Natsume --- .../tests/unit/compute/v2/test_server.py | 12 ++++++------ .../tests/unit/network/v2/test_local_ip.py | 14 +++++++------- .../tests/unit/network/v2/test_subnet.py | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index d393acdaf1..0760fc66a0 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -5101,8 +5101,8 @@ def test_server_list_with_locked(self): self.search_opts['locked'] = True self.servers_mock.list.assert_called_with(**self.kwargs) - self.assertItemsEqual(self.columns, columns) - self.assertItemsEqual(self.data, tuple(data)) + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, tuple(data)) def test_server_list_with_unlocked_v273(self): @@ -5121,8 +5121,8 @@ def test_server_list_with_unlocked_v273(self): self.search_opts['locked'] = False self.servers_mock.list.assert_called_with(**self.kwargs) - self.assertItemsEqual(self.columns, columns) - self.assertItemsEqual(self.data, tuple(data)) + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, tuple(data)) def test_server_list_with_locked_and_unlocked(self): @@ -5162,8 +5162,8 @@ def test_server_list_with_changes_before(self): self.servers_mock.list.assert_called_with(**self.kwargs) - self.assertItemsEqual(self.columns, columns) - self.assertItemsEqual(self.data, tuple(data)) + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, tuple(data)) @mock.patch.object(iso8601, 'parse_date', side_effect=iso8601.ParseError) def test_server_list_with_invalid_changes_before( diff --git a/openstackclient/tests/unit/network/v2/test_local_ip.py b/openstackclient/tests/unit/network/v2/test_local_ip.py index 97e246dfa2..be23365e08 100644 --- a/openstackclient/tests/unit/network/v2/test_local_ip.py +++ b/openstackclient/tests/unit/network/v2/test_local_ip.py @@ -96,7 +96,7 @@ def test_create_no_options(self): self.network.create_local_ip.assert_called_once_with(**{}) self.assertEqual(set(self.columns), set(columns)) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_all_options(self): arglist = [ @@ -130,7 +130,7 @@ def test_create_all_options(self): 'ip_mode': self.new_local_ip.ip_mode, }) self.assertEqual(set(self.columns), set(columns)) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) class TestDeleteLocalIP(TestLocalIP): @@ -263,7 +263,7 @@ def test_local_ip_list(self): self.network.local_ips.assert_called_once_with(**{}) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_local_ip_list_name(self): arglist = [ @@ -278,7 +278,7 @@ def test_local_ip_list_name(self): self.network.local_ips.assert_called_once_with( **{'name': self.local_ips[0].name}) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_local_ip_list_project(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -295,7 +295,7 @@ def test_local_ip_list_project(self): self.network.local_ips.assert_called_once_with( **{'project_id': project.id}) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_local_ip_project_domain(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -314,7 +314,7 @@ def test_local_ip_project_domain(self): self.network.local_ips.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_local_ip_list_network(self): arglist = [ @@ -477,4 +477,4 @@ def test_show_all_options(self): self.network.find_local_ip.assert_called_once_with( self._local_ip.name, ignore_missing=False) self.assertEqual(set(self.columns), set(columns)) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) diff --git a/openstackclient/tests/unit/network/v2/test_subnet.py b/openstackclient/tests/unit/network/v2/test_subnet.py index 6b3ab2cc07..7aaa583df8 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet.py +++ b/openstackclient/tests/unit/network/v2/test_subnet.py @@ -918,7 +918,7 @@ def test_subnet_list_subnetpool_by_name(self): self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_subnet_list_subnetpool_by_id(self): subnet_pool = network_fakes.FakeSubnetPool.create_one_subnet_pool() @@ -939,7 +939,7 @@ def test_subnet_list_subnetpool_by_id(self): self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_list_with_tag_options(self): arglist = [ From 28ac0141b56b8c0943fb29733c1fd6c000856b17 Mon Sep 17 00:00:00 2001 From: "Dr. Jens Harbott" Date: Tue, 20 Sep 2022 20:20:11 +0200 Subject: [PATCH 020/595] Run swift in -tips job Since there is only py3 left, swift has learned to live with it, so we might as well test it. Change-Id: Iab5232858e4a67e356680d169a885875d574c3cc --- .zuul.yaml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index d27ae3dae3..dc5ce44cea 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -149,22 +149,6 @@ vars: devstack_localrc: LIBS_FROM_GIT: python-openstackclient,openstacksdk,osc-lib,cliff - # This is insufficient, but leaving it here as a reminder of what may - # someday be all we need to make this work - # disable_python3_package swift - DISABLED_PYTHON3_PACKAGES: swift - devstack_services: - # Swift is not ready for python3 yet: At a minimum keystonemiddleware needs - # to be installed in the py2 env, there are probably other things too... - s-account: false - s-container: false - s-object: false - s-proxy: false - # As swift is not available for this job, c-bak service won't be functional. - # The backup related tests can be handled by other jobs having swift enabled. - # The backup service along with swift services can be enabled once swift is - # compatible with py3 - c-bak: false tox_envlist: functional tox_install_siblings: true From d163a2090471c8f90c8ad84827fb97dfbf06280b Mon Sep 17 00:00:00 2001 From: lsmman Date: Mon, 11 Oct 2021 22:52:52 +0900 Subject: [PATCH 021/595] image: Add 'image task show' commands This replaces and the 'glance task-show' command. For example: $ image task show Change-Id: I74cb23e436c373fe238804b903bbeb28f643d5af --- doc/source/cli/data/glance.csv | 2 +- openstackclient/image/v2/task.py | 78 ++++++++++++++++++ openstackclient/tests/unit/image/v2/fakes.py | 54 +++++++++++++ .../tests/unit/image/v2/test_task.py | 80 +++++++++++++++++++ ...-image-task-commands-50c3643ebfd0421f.yaml | 4 + setup.cfg | 1 + 6 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 openstackclient/image/v2/task.py create mode 100644 openstackclient/tests/unit/image/v2/test_task.py create mode 100644 releasenotes/notes/add-image-task-commands-50c3643ebfd0421f.yaml diff --git a/doc/source/cli/data/glance.csv b/doc/source/cli/data/glance.csv index 27585b9510..c44c50896f 100644 --- a/doc/source/cli/data/glance.csv +++ b/doc/source/cli/data/glance.csv @@ -55,6 +55,6 @@ stores-delete,,Delete image from specific store. stores-info,,Print available backends from Glance. task-create,,Create a new task. task-list,,List tasks you can access. -task-show,,Describe a specific task. +task-show,image task show,Describe a specific task. bash-completion,complete,Prints arguments for bash_completion. help,help,Display help about this program or one of its subcommands. diff --git a/openstackclient/image/v2/task.py b/openstackclient/image/v2/task.py new file mode 100644 index 0000000000..5f0c4ed5a7 --- /dev/null +++ b/openstackclient/image/v2/task.py @@ -0,0 +1,78 @@ +# 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. + +from osc_lib.cli import format_columns +from osc_lib.command import command + +from openstackclient.i18n import _ + + +def _format_task(task): + """Format an task to make it more consistent with OSC operations.""" + + info = {} + properties = {} + + # the only fields we're not including is "links", "tags" and the properties + fields_to_show = [ + 'created_at', + 'expires_at', + 'id', + 'input', + 'message', + 'owner_id', + 'result', + 'status', + 'type', + 'updated_at', + ] + + # split out the usual key and the properties which are top-level + for field in fields_to_show: + info[field] = task.get(field) + + for key in task: + if key in fields_to_show: + continue + + if key in {'location', 'name', 'schema'}: + continue + + properties[key] = task.get(key) + + # add properties back into the dictionary as a top-level key + info['properties'] = format_columns.DictColumn(properties) + + return info + + +class ShowTask(command.ShowOne): + _description = _('Display task details') + + def get_parser(self, prog_name): + parser = super(ShowTask, self).get_parser(prog_name) + + parser.add_argument( + 'task', + metavar='', + help=_('Task to display (ID)'), + ) + + return parser + + def take_action(self, parsed_args): + image_client = self.app.client_manager.image + + task = image_client.get_task(parsed_args.task) + info = _format_task(task) + + return zip(*sorted(info.items())) diff --git a/openstackclient/tests/unit/image/v2/fakes.py b/openstackclient/tests/unit/image/v2/fakes.py index a0eda6d23a..2decd12292 100644 --- a/openstackclient/tests/unit/image/v2/fakes.py +++ b/openstackclient/tests/unit/image/v2/fakes.py @@ -18,6 +18,7 @@ from openstack.image.v2 import image from openstack.image.v2 import member +from openstack.image.v2 import task from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes @@ -44,6 +45,9 @@ def __init__(self, **kwargs): self.remove_tag = mock.Mock() + self.tasks = mock.Mock() + self.get_task = mock.Mock() + self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] self.version = 2.0 @@ -129,3 +133,53 @@ def create_one_image_member(attrs=None): image_member_info.update(attrs) return member.Member(**image_member_info) + + +def create_one_task(attrs=None): + """Create a fake task. + + :param attrs: A dictionary with all attributes of task + :type attrs: dict + :return: A fake Task object. + :rtype: `openstack.image.v2.task.Task` + """ + attrs = attrs or {} + + # Set default attribute + task_info = { + 'created_at': '2016-06-29T16:13:07Z', + 'expires_at': '2016-07-01T16:13:07Z', + 'id': str(uuid.uuid4()), + 'input': { + 'image_properties': { + 'container_format': 'ovf', + 'disk_format': 'vhd' + }, + 'import_from': 'https://apps.openstack.org/excellent-image', + 'import_from_format': 'qcow2' + }, + 'message': '', + 'owner': str(uuid.uuid4()), + 'result': { + 'image_id': str(uuid.uuid4()), + }, + 'schema': '/v2/schemas/task', + 'status': random.choice( + [ + 'pending', + 'processing', + 'success', + 'failure', + ] + ), + # though not documented, the API only allows 'import' + # https://github.com/openstack/glance/blob/24.0.0/glance/api/v2/tasks.py#L186-L190 + 'type': 'import', + 'updated_at': '2016-06-29T16:13:07Z', + + } + + # Overwrite default attributes if there are some attributes set + task_info.update(attrs) + + return task.Task(**task_info) diff --git a/openstackclient/tests/unit/image/v2/test_task.py b/openstackclient/tests/unit/image/v2/test_task.py new file mode 100644 index 0000000000..90fa11d89e --- /dev/null +++ b/openstackclient/tests/unit/image/v2/test_task.py @@ -0,0 +1,80 @@ +# 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. + +from osc_lib.cli import format_columns + +from openstackclient.image.v2 import task +from openstackclient.tests.unit.image.v2 import fakes as image_fakes + + +class TestTask(image_fakes.TestImagev2): + def setUp(self): + super().setUp() + + # Get shortcuts to mocked image client + self.client = self.app.client_manager.image + + +class TestTaskShow(TestTask): + + task = image_fakes.create_one_task() + + columns = ( + 'created_at', + 'expires_at', + 'id', + 'input', + 'message', + 'owner_id', + 'properties', + 'result', + 'status', + 'type', + 'updated_at', + ) + data = ( + task.created_at, + task.expires_at, + task.id, + task.input, + task.message, + task.owner_id, + format_columns.DictColumn({}), + task.result, + task.status, + task.type, + task.updated_at, + ) + + def setUp(self): + super().setUp() + + self.client.get_task.return_value = self.task + + # Get the command object to test + self.cmd = task.ShowTask(self.app, None) + + def test_task_show(self): + arglist = [self.task.id] + verifylist = [ + ('task', self.task.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + self.client.get_task.assert_called_with(self.task.id) + + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) diff --git a/releasenotes/notes/add-image-task-commands-50c3643ebfd0421f.yaml b/releasenotes/notes/add-image-task-commands-50c3643ebfd0421f.yaml new file mode 100644 index 0000000000..927f6a80db --- /dev/null +++ b/releasenotes/notes/add-image-task-commands-50c3643ebfd0421f.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add ``image task show`` command to show a task for the image service. diff --git a/setup.cfg b/setup.cfg index fe5db3f2c3..1d00ec33af 100644 --- a/setup.cfg +++ b/setup.cfg @@ -382,6 +382,7 @@ openstack.image.v2 = image_show = openstackclient.image.v2.image:ShowImage image_set = openstackclient.image.v2.image:SetImage image_unset = openstackclient.image.v2.image:UnsetImage + image_task_show = openstackclient.image.v2.task:ShowTask openstack.network.v2 = address_group_create = openstackclient.network.v2.address_group:CreateAddressGroup From c9d445fc4baf036793103b15c9eb2632da3610e0 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 7 Jul 2022 12:31:44 +0100 Subject: [PATCH 022/595] image: Add 'image task list' command This replaces the 'glance task-list' command. $ openstack image task list We also indicate that the 'image task create' command will never be implemented. This is an admin-only API that isn't really intended to be used by humans thus it does not need an OSC command implementation. Change-Id: Id8a943a5443782fc70c0fbf3639f5aa17b9d30af --- doc/source/cli/data/glance.csv | 4 +- openstackclient/image/v2/task.py | 101 +++++++++++++++++ openstackclient/tests/unit/image/v2/fakes.py | 21 +++- .../tests/unit/image/v2/test_task.py | 107 ++++++++++++++++++ ...-image-task-commands-50c3643ebfd0421f.yaml | 2 + setup.cfg | 1 + 6 files changed, 233 insertions(+), 3 deletions(-) diff --git a/doc/source/cli/data/glance.csv b/doc/source/cli/data/glance.csv index c44c50896f..7dd967c914 100644 --- a/doc/source/cli/data/glance.csv +++ b/doc/source/cli/data/glance.csv @@ -53,8 +53,8 @@ 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. stores-info,,Print available backends from Glance. -task-create,,Create a new task. -task-list,,List tasks you can access. +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. bash-completion,complete,Prints arguments for bash_completion. help,help,Display help about this program or one of its subcommands. diff --git a/openstackclient/image/v2/task.py b/openstackclient/image/v2/task.py index 5f0c4ed5a7..924eaaf13d 100644 --- a/openstackclient/image/v2/task.py +++ b/openstackclient/image/v2/task.py @@ -12,9 +12,14 @@ from osc_lib.cli import format_columns from osc_lib.command import command +from osc_lib import utils from openstackclient.i18n import _ +_formatters = { + 'tags': format_columns.ListColumn, +} + def _format_task(task): """Format an task to make it more consistent with OSC operations.""" @@ -76,3 +81,99 @@ def take_action(self, parsed_args): info = _format_task(task) return zip(*sorted(info.items())) + + +class ListTask(command.Lister): + _description = _('List tasks') + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + + parser.add_argument( + '--sort-key', + metavar='[:]', + help=_( + 'Sorts the response by one of the following attributes: ' + 'created_at, expires_at, id, status, type, updated_at. ' + '(default is created_at) ' + '(multiple keys and directions can be specified separated ' + 'by comma)' + ), + ) + parser.add_argument( + '--sort-dir', + metavar='[:]', + help=_( + 'Sort output by selected keys and directions (asc or desc) ' + '(default: name:desc) ' + '(multiple keys and directions can be specified separated ' + 'by comma)' + ), + ) + parser.add_argument( + '--limit', + metavar='', + type=int, + help=_('Maximum number of tasks to display.'), + ) + parser.add_argument( + '--marker', + metavar='', + help=_( + 'The last task of the previous page. ' + 'Display list of tasks after marker. ' + 'Display all tasks if not specified. ' + '(name or ID)' + ), + ) + parser.add_argument( + '--type', + metavar='', + choices=['import'], + help=_('Filters the response by a task type.'), + ) + parser.add_argument( + '--status', + metavar='', + choices=[ + 'pending', + 'processing', + 'success', + 'failure', + ], + help=_('Filter tasks based on status.'), + ) + + return parser + + def take_action(self, parsed_args): + image_client = self.app.client_manager.image + + columns = ('id', 'type', 'status', 'owner_id') + column_headers = ('ID', 'Type', 'Status', 'Owner') + + kwargs = {} + copy_attrs = { + 'sort_key', + 'sort_dir', + 'limit', + 'marker', + 'type', + 'status', + } + for attr in copy_attrs: + val = getattr(parsed_args, attr, None) + if val is not None: + # Only include a value in kwargs for attributes that are + # actually present on the command line + kwargs[attr] = val + + data = image_client.tasks(**kwargs) + + return ( + column_headers, + ( + utils.get_item_properties(s, columns, formatters=_formatters) + for s in data + ), + ) diff --git a/openstackclient/tests/unit/image/v2/fakes.py b/openstackclient/tests/unit/image/v2/fakes.py index 2decd12292..f20154502e 100644 --- a/openstackclient/tests/unit/image/v2/fakes.py +++ b/openstackclient/tests/unit/image/v2/fakes.py @@ -52,6 +52,9 @@ def __init__(self, **kwargs): self.management_url = kwargs['endpoint'] self.version = 2.0 + self.tasks = mock.Mock() + self.tasks.resource_class = fakes.FakeResource(None, {}) + class TestImagev2(utils.TestCommand): @@ -176,10 +179,26 @@ def create_one_task(attrs=None): # https://github.com/openstack/glance/blob/24.0.0/glance/api/v2/tasks.py#L186-L190 'type': 'import', 'updated_at': '2016-06-29T16:13:07Z', - } # Overwrite default attributes if there are some attributes set task_info.update(attrs) return task.Task(**task_info) + + +def create_tasks(attrs=None, count=2): + """Create multiple fake tasks. + + :param attrs: A dictionary with all attributes of Task + :type attrs: dict + :param count: The number of tasks to be faked + :type count: int + :return: A list of fake Task objects + :rtype: list + """ + tasks = [] + for n in range(0, count): + tasks.append(create_one_task(attrs)) + + return tasks diff --git a/openstackclient/tests/unit/image/v2/test_task.py b/openstackclient/tests/unit/image/v2/test_task.py index 90fa11d89e..e077e2b140 100644 --- a/openstackclient/tests/unit/image/v2/test_task.py +++ b/openstackclient/tests/unit/image/v2/test_task.py @@ -78,3 +78,110 @@ def test_task_show(self): self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, data) + + +class TestTaskList(TestTask): + + tasks = image_fakes.create_tasks() + + columns = ( + 'ID', + 'Type', + 'Status', + 'Owner', + ) + datalist = [ + ( + task.id, + task.type, + task.status, + task.owner_id, + ) + for task in tasks + ] + + def setUp(self): + super().setUp() + + self.client.tasks.side_effect = [self.tasks, []] + + # Get the command object to test + self.cmd = task.ListTask(self.app, None) + + def test_task_list_no_options(self): + arglist = [] + verifylist = [ + ('sort_key', None), + ('sort_dir', None), + ('limit', None), + ('marker', None), + ('type', None), + ('status', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.client.tasks.assert_called_with() + + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.datalist, data) + + def test_task_list_sort_key_option(self): + arglist = ['--sort-key', 'created_at'] + verifylist = [('sort_key', 'created_at')] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.client.tasks.assert_called_with( + sort_key=parsed_args.sort_key, + ) + + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.datalist, data) + + def test_task_list_sort_dir_option(self): + arglist = ['--sort-dir', 'desc'] + verifylist = [('sort_dir', 'desc')] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.client.tasks.assert_called_with( + sort_dir=parsed_args.sort_dir, + ) + + def test_task_list_pagination_options(self): + arglist = ['--limit', '1', '--marker', self.tasks[0].id] + verifylist = [('limit', 1), ('marker', self.tasks[0].id)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.client.tasks.assert_called_with( + limit=parsed_args.limit, + marker=parsed_args.marker, + ) + + def test_task_list_type_option(self): + arglist = ['--type', self.tasks[0].type] + verifylist = [('type', self.tasks[0].type)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.client.tasks.assert_called_with( + type=self.tasks[0].type, + ) + + def test_task_list_status_option(self): + arglist = ['--status', self.tasks[0].status] + verifylist = [('status', self.tasks[0].status)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.client.tasks.assert_called_with( + status=self.tasks[0].status, + ) diff --git a/releasenotes/notes/add-image-task-commands-50c3643ebfd0421f.yaml b/releasenotes/notes/add-image-task-commands-50c3643ebfd0421f.yaml index 927f6a80db..1e4419792a 100644 --- a/releasenotes/notes/add-image-task-commands-50c3643ebfd0421f.yaml +++ b/releasenotes/notes/add-image-task-commands-50c3643ebfd0421f.yaml @@ -2,3 +2,5 @@ features: - | Add ``image task show`` command to show a task for the image service. + - | + Add ``image task list`` command to list tasks for the image service. diff --git a/setup.cfg b/setup.cfg index 1d00ec33af..fd43d1ab84 100644 --- a/setup.cfg +++ b/setup.cfg @@ -383,6 +383,7 @@ openstack.image.v2 = image_set = openstackclient.image.v2.image:SetImage image_unset = openstackclient.image.v2.image:UnsetImage image_task_show = openstackclient.image.v2.task:ShowTask + image_task_list = openstackclient.image.v2.task:ListTask openstack.network.v2 = address_group_create = openstackclient.network.v2.address_group:CreateAddressGroup From ff273909da631566919392a07e1e758609476c54 Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Mon, 12 Sep 2022 21:21:49 +0900 Subject: [PATCH 023/595] Fix wrong assertion methods Change-Id: Iebf4840662e87e59a06b56f1b5248728cba03c24 Story: 2010288 Task: 46261 Signed-off-by: Takashi Natsume --- openstackclient/tests/unit/compute/v2/test_server.py | 2 +- openstackclient/tests/unit/image/v2/test_image.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index d393acdaf1..138ac345df 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -4634,7 +4634,7 @@ def test_server_list_with_flavor(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.flavors_mock.get.has_calls(self.flavor.id) + self.flavors_mock.get.assert_has_calls([mock.call(self.flavor.id)]) self.search_opts['flavor'] = self.flavor.id self.servers_mock.list.assert_called_with(**self.kwargs) diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index 02f7b4118f..f2c113649c 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -1090,7 +1090,7 @@ def test_image_set_no_options(self): self.assertIsNone(result) # we'll have called this but not set anything - self.app.client_manager.image.update_image.called_once_with( + self.app.client_manager.image.update_image.assert_called_once_with( self._image.id, ) From 1b0d2dde13c0a2d327ef2f5ca98d073a45595e71 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 23 Sep 2022 14:15:52 +0000 Subject: [PATCH 024/595] Update master for stable/zed Add file to the reno documentation build to show release notes for stable/zed. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/zed. Sem-Ver: feature Change-Id: Ie570af5ce752d91190ebf6aa3c5ff2329afa36d3 --- releasenotes/source/index.rst | 1 + releasenotes/source/zed.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/zed.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 829b09d022..69562dbd8e 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ OpenStackClient Release Notes :maxdepth: 1 unreleased + zed yoga xena wallaby diff --git a/releasenotes/source/zed.rst b/releasenotes/source/zed.rst new file mode 100644 index 0000000000..9608c05e45 --- /dev/null +++ b/releasenotes/source/zed.rst @@ -0,0 +1,6 @@ +======================== +Zed Series Release Notes +======================== + +.. release-notes:: + :branch: stable/zed From 00d8d945a1869b723b9845b3e739928a7c4da00a Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 23 Sep 2022 14:15:54 +0000 Subject: [PATCH 025/595] Switch to 2023.1 Python3 unit tests and generic template name This is an automatically generated patch to ensure unit testing is in place for all the of the tested runtimes for antelope. Also, updating the template name to generic one. See also the PTI in governance [1]. [1]: https://governance.openstack.org/tc/reference/project-testing-interface.html Change-Id: Ie3677749177a32507007833717b9bc484ce2c493 --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index d27ae3dae3..01affb958c 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -240,7 +240,7 @@ - openstackclient-plugin-jobs - osc-tox-unit-tips - openstack-cover-jobs - - openstack-python3-zed-jobs + - openstack-python3-jobs - publish-openstack-docs-pti - check-requirements - release-notes-jobs-python3 From 390f8135bdf837ae931bcebc3f4ffdb8a9daae8b Mon Sep 17 00:00:00 2001 From: Bence Romsics Date: Wed, 21 Sep 2022 15:20:44 +0200 Subject: [PATCH 026/595] Improve help text for network create --external Change-Id: Ie802fbb8a35e53dcb79b5581f64caa2db1609480 Closes-Bug: #1990285 --- openstackclient/network/v2/network.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index d7b37ebede..6a7c88baf5 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -271,14 +271,16 @@ def update_parser_network(self, parser): '--external', action='store_true', help=self.enhance_help_neutron( - _("Set this network as an external network " + _("The network has an external routing facility that's not " + "managed by Neutron and can be used as in: " + "openstack router set --external-gateway NETWORK " "(external-net extension required)")) ) external_router_grp.add_argument( '--internal', action='store_true', help=self.enhance_help_neutron( - _("Set this network as an internal network (default)")) + _("Opposite of '--external' (default)")) ) default_router_grp = parser.add_mutually_exclusive_group() default_router_grp.add_argument( @@ -690,13 +692,15 @@ def get_parser(self, prog_name): external_router_grp.add_argument( '--external', action='store_true', - help=_("Set this network as an external network " + help=_("The network has an external routing facility that's not " + "managed by Neutron and can be used as in: " + "openstack router set --external-gateway NETWORK " "(external-net extension required)") ) external_router_grp.add_argument( '--internal', action='store_true', - help=_("Set this network as an internal network") + help=_("Opposite of '--external'") ) default_router_grp = parser.add_mutually_exclusive_group() default_router_grp.add_argument( From bfdf900d0a2fae16fe1ff2d8604215dd2de01d4a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 22 Sep 2022 18:07:09 +0100 Subject: [PATCH 027/595] doc: Update cinder command mapping I had incorrectly marked the 'api-version' command as WONTFIX. Turns out we already support this, albeit via keystone as opposed to a cinder-specific API. Change-Id: I804ef1ca5a0f4df844d8c2229688c4edbac45aa9 Signed-off-by: Stephen Finucane --- doc/source/cli/data/cinder.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/cli/data/cinder.csv b/doc/source/cli/data/cinder.csv index 3727796692..f3da43eb2e 100644 --- a/doc/source/cli/data/cinder.csv +++ b/doc/source/cli/data/cinder.csv @@ -1,5 +1,5 @@ absolute-limits,limits show --absolute,Lists absolute limits for a user. -api-version,WONTFIX,Display the server API version information. +api-version,openstack versions show --service volume,Display the server API version information. availability-zone-list,availability zone list --volume,Lists all availability zones. attachment-complete,volume attachment complete,Complete an attachment for a cinder volume. (Supported by API versions 3.44 - 3.latest) attachment-create,volume attachment create,Create an attachment for a cinder volume. (Supported by API versions 3.27 - 3.latest) From 45bec041b206678de36f2f463ac6872b785e592e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 22 Sep 2022 18:09:08 +0100 Subject: [PATCH 028/595] quota: Add 'quota delete' command We had previously indicated that people use 'quota set' for unsetting a project's quotas, however, there was nothing that actually allowed us to do this. Correct that oversight. Change-Id: I04057e766b8ccf94bf219972249b68dc2bb796d4 Signed-off-by: Stephen Finucane --- doc/source/cli/data/cinder.csv | 2 +- doc/source/cli/data/neutron.csv | 2 +- doc/source/cli/data/nova.csv | 2 +- openstackclient/common/quota.py | 79 +++++++++++++ .../tests/unit/common/test_quota.py | 107 ++++++++++++++++++ .../notes/quota-delete-947df66ae5341cbf.yaml | 5 + setup.cfg | 1 + tox.ini | 4 +- 8 files changed, 197 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/quota-delete-947df66ae5341cbf.yaml diff --git a/doc/source/cli/data/cinder.csv b/doc/source/cli/data/cinder.csv index f3da43eb2e..310ae172b0 100644 --- a/doc/source/cli/data/cinder.csv +++ b/doc/source/cli/data/cinder.csv @@ -91,7 +91,7 @@ qos-show,volume qos show,Shows a specified qos specs. quota-class-show,quota show --class,Lists quotas for a quota class. quota-class-update,quota set --class,Updates quotas for a quota class. quota-defaults,quota show --default,Lists default quotas for a tenant. -quota-delete,,Delete the quotas for a tenant. +quota-delete,quota delete --volume,Delete the quotas for a tenant. quota-show,quota show,Lists quotas for a tenant. quota-update,quota set,Updates quotas for a tenant. quota-usage,,Lists quota usage for a tenant. diff --git a/doc/source/cli/data/neutron.csv b/doc/source/cli/data/neutron.csv index 402f4064ee..f0caa4797b 100644 --- a/doc/source/cli/data/neutron.csv +++ b/doc/source/cli/data/neutron.csv @@ -188,7 +188,7 @@ qos-policy-list,network qos policy list,List QoS policies that belong to a given qos-policy-show,network qos policy show,Show information of a given qos policy. qos-policy-update,network qos policy set,Update a given qos policy. quota-default-show,quota show --default,Show default quotas for a given tenant. -quota-delete,,Delete defined quotas of a given tenant. +quota-delete,quota delete --network,Delete defined quotas of a given tenant. quota-list,quota list,List quotas of all tenants who have non-default quota values. quota-show,quota show,Show quotas for a given tenant. quota-update,quota set,Define tenant's quotas not to use defaults. diff --git a/doc/source/cli/data/nova.csv b/doc/source/cli/data/nova.csv index 4252585f9b..2909bc0c6c 100644 --- a/doc/source/cli/data/nova.csv +++ b/doc/source/cli/data/nova.csv @@ -70,7 +70,7 @@ pause,server pause,Pause a server. quota-class-show,quota show --class,List the quotas for a quota class. quota-class-update,quota set --class,Update the quotas for a quota class. quota-defaults,quota list,List the default quotas for a tenant. -quota-delete,quota set,Delete quota for a tenant/user so their quota will Revert back to default. +quota-delete,quota delete --compute,Delete quota for a tenant/user so their quota will Revert back to default. quota-show,quota show,List the quotas for a tenant/user. quota-update,quota set,Update the quotas for a tenant/user. reboot,server reboot,Reboot a server. diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 44482367e7..0110feb668 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -697,3 +697,82 @@ def take_action(self, parsed_args): info['project_name'] = project_name return zip(*sorted(info.items())) + + +class DeleteQuota(command.Command): + _description = _( + "Delete configured quota for a project and revert to defaults." + ) + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'project', + metavar='', + help=_('Delete quotas for this project (name or ID)'), + ) + option = parser.add_mutually_exclusive_group() + option.add_argument( + '--all', + action='store_const', + const='all', + dest='service', + default='all', + help=_('Delete project quotas for all services (default)'), + ) + option.add_argument( + '--compute', + action='store_const', + const='compute', + dest='service', + default='all', + help=_( + 'Delete compute quotas for the project ' + '(including network quotas when using nova-network)' + ), + ) + option.add_argument( + '--volume', + action='store_const', + const='volume', + dest='service', + default='all', + help=_('Delete volume quotas for the project'), + ) + option.add_argument( + '--network', + action='store_const', + const='network', + dest='service', + default='all', + help=_('Delete network quotas for the project'), + ) + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ) + + # compute quotas + if parsed_args.service in {'all', 'compute'}: + compute_client = self.app.client_manager.compute + compute_client.quotas.delete(project) + + # volume quotas + if parsed_args.service in {'all', 'volume'}: + volume_client = self.app.client_manager.volume + volume_client.quotas.delete(project) + + # network quotas (but only if we're not using nova-network, otherwise + # we already deleted the quotas in the compute step) + if ( + parsed_args.service in {'all', 'network'} + and self.app.client_manager.is_network_endpoint_enabled() + ): + network_client = self.app.client_manager.network + network_client.quotas.delete(project) + + return None diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index 70fd1436a0..087443c169 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -62,6 +62,9 @@ def setUp(self): self.app.client_manager.volume.quota_classes self.volume_quotas_class_mock.reset_mock() + self.app.client_manager.network.quotas = mock.Mock() + self.network_quotas_mock = self.app.client_manager.network.quotas + self.app.client_manager.auth_ref = mock.Mock() self.app.client_manager.auth_ref.service_catalog = mock.Mock() self.service_catalog_mock = \ @@ -1154,3 +1157,107 @@ def test_network_quota_show_remove_empty(self): self.assertEqual(len(network_fakes.QUOTA) - 1, len(result)) # Go back to default mock self.network.get_quota = orig_get_quota + + +class TestQuotaDelete(TestQuota): + """Test cases for quota delete command""" + + def setUp(self): + super().setUp() + + self.cmd = quota.DeleteQuota(self.app, None) + + def test_delete(self): + """Delete all quotas""" + arglist = [ + self.projects[0].id, + ] + verifylist = [ + ('service', 'all'), + ('project', self.projects[0].id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) + self.projects_mock.get.assert_called_once_with(self.projects[0].id) + self.compute_quotas_mock.delete.assert_called_once_with( + self.projects[0], + ) + self.volume_quotas_mock.delete.assert_called_once_with( + self.projects[0], + ) + self.network_quotas_mock.delete.assert_called_once_with( + self.projects[0], + ) + + def test_delete__compute(self): + """Delete compute quotas only""" + arglist = [ + '--compute', + self.projects[0].id, + ] + verifylist = [ + ('service', 'compute'), + ('project', self.projects[0].id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) + self.projects_mock.get.assert_called_once_with(self.projects[0].id) + self.compute_quotas_mock.delete.assert_called_once_with( + self.projects[0], + ) + self.volume_quotas_mock.delete.assert_not_called() + self.network_quotas_mock.delete.assert_not_called() + + def test_delete__volume(self): + """Delete volume quotas only""" + arglist = [ + '--volume', + self.projects[0].id, + ] + verifylist = [ + ('service', 'volume'), + ('project', self.projects[0].id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) + self.projects_mock.get.assert_called_once_with(self.projects[0].id) + self.compute_quotas_mock.delete.assert_not_called() + self.volume_quotas_mock.delete.assert_called_once_with( + self.projects[0], + ) + self.network_quotas_mock.delete.assert_not_called() + + def test_delete__network(self): + """Delete network quotas only""" + arglist = [ + '--network', + self.projects[0].id, + ] + verifylist = [ + ('service', 'network'), + ('project', self.projects[0].id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) + self.projects_mock.get.assert_called_once_with(self.projects[0].id) + self.compute_quotas_mock.delete.assert_not_called() + self.volume_quotas_mock.delete.assert_not_called() + self.network_quotas_mock.delete.assert_called_once_with( + self.projects[0], + ) diff --git a/releasenotes/notes/quota-delete-947df66ae5341cbf.yaml b/releasenotes/notes/quota-delete-947df66ae5341cbf.yaml new file mode 100644 index 0000000000..0d992451c3 --- /dev/null +++ b/releasenotes/notes/quota-delete-947df66ae5341cbf.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added a new command, ``quota delete``, that will allow admins delete quotas + set for projects. Supported by the compute, volume, and network services. diff --git a/setup.cfg b/setup.cfg index fe5db3f2c3..c8935c5860 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,6 +49,7 @@ openstack.common = quota_list = openstackclient.common.quota:ListQuota quota_set = openstackclient.common.quota:SetQuota quota_show = openstackclient.common.quota:ShowQuota + quota_delete = openstackclient.common.quota:DeleteQuota versions_show = openstackclient.common.versions:ShowVersions openstack.compute.v2 = diff --git a/tox.ini b/tox.ini index f631380df5..5f02e7c21c 100644 --- a/tox.ini +++ b/tox.ini @@ -134,7 +134,7 @@ show-source = True # H203: Use assertIs(Not)None to check for None enable-extensions = H203 exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,tools,releasenotes -# W504 is disabled since you must choose between this or W503 -ignore = W504 +# W503 and W504 are disabled since they're not very useful +ignore = W503,W504 import-order-style = pep8 application_import_names = openstackclient From 44443f78561ce4f23d202a42de4a4ceac2ffa097 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 23 Sep 2022 15:24:43 +0100 Subject: [PATCH 029/595] quota: Add support for detailed volume quotas We were stating that this was not supported. That is not true. Correct the oversight. Change-Id: Ib9d9db641a18e142be0a1eccff783e7cccdf2db5 Signed-off-by: Stephen Finucane --- doc/source/cli/data/cinder.csv | 2 +- openstackclient/common/quota.py | 26 +++++++++---- .../tests/unit/common/test_quota.py | 37 +++++++++++++++++-- openstackclient/tests/unit/volume/v2/fakes.py | 29 +++++++++++++++ ...tailed-volume-quotas-198dc2e8f57ce1e7.yaml | 7 ++++ 5 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 releasenotes/notes/detailed-volume-quotas-198dc2e8f57ce1e7.yaml diff --git a/doc/source/cli/data/cinder.csv b/doc/source/cli/data/cinder.csv index 310ae172b0..9fe6c9d685 100644 --- a/doc/source/cli/data/cinder.csv +++ b/doc/source/cli/data/cinder.csv @@ -94,7 +94,7 @@ quota-defaults,quota show --default,Lists default quotas for a tenant. quota-delete,quota delete --volume,Delete the quotas for a tenant. quota-show,quota show,Lists quotas for a tenant. quota-update,quota set,Updates quotas for a tenant. -quota-usage,,Lists quota usage for a tenant. +quota-usage,quota list --detail,Lists quota usage for a tenant. rate-limits,limits show --rate,Lists rate limits for a user. readonly-mode-update,volume set --read-only-mode | --read-write-mode,Updates volume read-only access-mode flag. rename,volume set --name,Renames a volume. diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 0110feb668..8477da9079 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -143,6 +143,7 @@ def get_compute_quota(self, client, parsed_args): def get_volume_quota(self, client, parsed_args): quota_class = ( parsed_args.quota_class if 'quota_class' in parsed_args else False) + detail = parsed_args.detail if 'detail' in parsed_args else False default = parsed_args.default if 'default' in parsed_args else False try: if quota_class: @@ -153,7 +154,7 @@ def get_volume_quota(self, client, parsed_args): if default: quota = client.quotas.defaults(project) else: - quota = client.quotas.get(project) + quota = client.quotas.get(project, usage=detail) except Exception as e: if type(e).__name__ == 'EndpointNotFound': return {} @@ -195,7 +196,7 @@ def get_network_quota(self, parsed_args): # more consistent for key, values in network_quota.items(): if type(values) is dict and "used" in values: - values[u'in_use'] = values.pop("used") + values['in_use'] = values.pop("used") network_quota[key] = values return network_quota else: @@ -205,7 +206,8 @@ def get_network_quota(self, parsed_args): class ListQuota(command.Lister, BaseQuota): _description = _( "List quotas for all projects with non-default quota values or " - "list detailed quota information for requested project") + "list detailed quota information for requested project" + ) def _get_detailed_quotas(self, parsed_args): columns = ( @@ -222,10 +224,21 @@ def _get_detailed_quotas(self, parsed_args): ) quotas = {} if parsed_args.compute: - quotas.update(self.get_compute_quota( - self.app.client_manager.compute, parsed_args)) + quotas.update( + self.get_compute_quota( + self.app.client_manager.compute, + parsed_args, + ) + ) if parsed_args.network: quotas.update(self.get_network_quota(parsed_args)) + if parsed_args.volume: + quotas.update( + self.get_volume_quota( + self.app.client_manager.volume, + parsed_args, + ), + ) result = [] for resource, values in quotas.items(): @@ -359,8 +372,7 @@ def take_action(self, parsed_args): if parsed_args.volume: if parsed_args.detail: - LOG.warning("Volume service doesn't provide detailed quota" - " information") + return self._get_detailed_quotas(parsed_args) volume_client = self.app.client_manager.volume for p in project_ids: try: diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index 087443c169..580072086d 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -279,6 +279,37 @@ def test_quota_list_details_network(self): self.assertEqual( sorted(detailed_reference_data), sorted(ret_quotas)) + def test_quota_list_details_volume(self): + detailed_quota = ( + volume_fakes.FakeQuota.create_one_detailed_quota()) + + detailed_column_header = ( + 'Resource', + 'In Use', + 'Reserved', + 'Limit', + ) + detailed_reference_data = ( + self._get_detailed_reference_data(detailed_quota)) + + self.volume.quotas.get = mock.Mock(return_value=detailed_quota) + + arglist = [ + '--detail', + '--volume', + ] + verifylist = [ + ('detail', True), + ('volume', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + ret_quotas = list(data) + + self.assertEqual(detailed_column_header, columns) + self.assertEqual(sorted(detailed_reference_data), sorted(ret_quotas)) + def test_quota_list_compute(self): # Two projects with non-default quotas self.compute.quotas.get = mock.Mock( @@ -1001,7 +1032,7 @@ def test_quota_set_with_check_limit(self): class TestQuotaShow(TestQuota): def setUp(self): - super(TestQuotaShow, self).setUp() + super().setUp() self.compute_quota = compute_fakes.FakeQuota.create_one_comp_quota() self.compute_quotas_mock.get.return_value = self.compute_quota @@ -1066,7 +1097,7 @@ def test_quota_show(self): self.projects[0].id, detail=False ) self.volume_quotas_mock.get.assert_called_once_with( - self.projects[0].id, + self.projects[0].id, usage=False ) self.network.get_quota.assert_called_once_with( self.projects[0].id, details=False @@ -1128,7 +1159,7 @@ def test_quota_show_no_project(self): identity_fakes.project_id, detail=False ) self.volume_quotas_mock.get.assert_called_once_with( - identity_fakes.project_id, + identity_fakes.project_id, usage=False ) self.network.get_quota.assert_called_once_with( identity_fakes.project_id, details=False diff --git a/openstackclient/tests/unit/volume/v2/fakes.py b/openstackclient/tests/unit/volume/v2/fakes.py index 96e381d3cc..6da69f8fcc 100644 --- a/openstackclient/tests/unit/volume/v2/fakes.py +++ b/openstackclient/tests/unit/volume/v2/fakes.py @@ -1193,6 +1193,35 @@ def create_one_default_vol_quota(attrs=None): return quota + @staticmethod + def create_one_detailed_quota(attrs=None): + """Create one quota""" + attrs = attrs or {} + + quota_attrs = { + 'volumes': {'limit': 3, 'in_use': 1, 'reserved': 0}, + 'per_volume_gigabytes': {'limit': -1, 'in_use': 0, 'reserved': 0}, + 'snapshots': {'limit': 10, 'in_use': 0, 'reserved': 0}, + 'gigabytes': {'limit': 1000, 'in_use': 5, 'reserved': 0}, + 'backups': {'limit': 10, 'in_use': 0, 'reserved': 0}, + 'backup_gigabytes': {'limit': 1000, 'in_use': 0, 'reserved': 0}, + 'volumes_lvmdriver-1': {'limit': -1, 'in_use': 1, 'reserved': 0}, + 'gigabytes_lvmdriver-1': {'limit': -1, 'in_use': 5, 'reserved': 0}, + 'snapshots_lvmdriver-1': {'limit': -1, 'in_use': 0, 'reserved': 0}, + 'volumes___DEFAULT__': {'limit': -1, 'in_use': 0, 'reserved': 0}, + 'gigabytes___DEFAULT__': {'limit': -1, 'in_use': 0, 'reserved': 0}, + 'snapshots___DEFAULT__': {'limit': -1, 'in_use': 0, 'reserved': 0}, + 'groups': {'limit': 10, 'in_use': 0, 'reserved': 0}, + 'id': uuid.uuid4().hex, + } + quota_attrs.update(attrs) + + quota = fakes.FakeResource( + info=copy.deepcopy(quota_attrs), + loaded=True) + + return quota + class FakeLimits(object): """Fake limits""" diff --git a/releasenotes/notes/detailed-volume-quotas-198dc2e8f57ce1e7.yaml b/releasenotes/notes/detailed-volume-quotas-198dc2e8f57ce1e7.yaml new file mode 100644 index 0000000000..bd0ef8522b --- /dev/null +++ b/releasenotes/notes/detailed-volume-quotas-198dc2e8f57ce1e7.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + The ``quota list`` command can now provide detailed quotas for the volume + service, e.g.:: + + $ openstack quota list --detail --volume From ec8dba29f9f646ec05c0d6fad32b4b3aaf99f6af Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Fri, 5 Aug 2022 14:03:21 +0300 Subject: [PATCH 030/595] Speed up standard flavor list command currently this command tries to fetch extra_specs for any flavor that does not have them (which is quite usual), regardless if the command was even asked to display them (--long) at all. This significantly slows down this command as it makes a lot of unnecessary REST calls, one per each flavor to fetch extra_specs for. With this patch, client only attempts to fetch flavor extra_specs if the user actually called the client with --long. Change-Id: Ia36414d891a41b641d7a9a04f0a1e7d43cfee351 Story: 2010343 Task: 46484 --- openstackclient/compute/v2/flavor.py | 2 +- .../tests/unit/compute/v2/test_flavor.py | 57 +++++++++++++++++++ .../notes/story-2010343-b5eb4ed593f51d3f.yaml | 8 +++ 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/story-2010343-b5eb4ed593f51d3f.yaml diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index 8a9eb07ad1..bc8f758bb1 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -333,7 +333,7 @@ def take_action(self, parsed_args): # Even if server supports 2.61 some policy might stop it sending us # extra_specs. So try to fetch them if they are absent for f in data: - if not f.extra_specs: + if parsed_args.long and not f.extra_specs: compute_client.fetch_flavor_extra_specs(f) columns = ( diff --git a/openstackclient/tests/unit/compute/v2/test_flavor.py b/openstackclient/tests/unit/compute/v2/test_flavor.py index 14dd3df20a..33ebf54622 100644 --- a/openstackclient/tests/unit/compute/v2/test_flavor.py +++ b/openstackclient/tests/unit/compute/v2/test_flavor.py @@ -523,6 +523,7 @@ def test_flavor_list_no_options(self): self.sdk_client.flavors.assert_called_with( **kwargs ) + self.sdk_client.fetch_flavor_extra_specs.assert_not_called() self.assertEqual(self.columns, columns) self.assertEqual(self.data, tuple(data)) @@ -550,6 +551,7 @@ def test_flavor_list_all_flavors(self): self.sdk_client.flavors.assert_called_with( **kwargs ) + self.sdk_client.fetch_flavor_extra_specs.assert_not_called() self.assertEqual(self.columns, columns) self.assertEqual(self.data, tuple(data)) @@ -577,6 +579,7 @@ def test_flavor_list_private_flavors(self): self.sdk_client.flavors.assert_called_with( **kwargs ) + self.sdk_client.fetch_flavor_extra_specs.assert_not_called() self.assertEqual(self.columns, columns) self.assertEqual(self.data, tuple(data)) @@ -604,6 +607,7 @@ def test_flavor_list_public_flavors(self): self.sdk_client.flavors.assert_called_with( **kwargs ) + self.sdk_client.fetch_flavor_extra_specs.assert_not_called() self.assertEqual(self.columns, columns) self.assertEqual(self.data, tuple(data)) @@ -631,6 +635,58 @@ def test_flavor_list_long(self): self.sdk_client.flavors.assert_called_with( **kwargs ) + self.sdk_client.fetch_flavor_extra_specs.assert_not_called() + + self.assertEqual(self.columns_long, columns) + self.assertCountEqual(self.data_long, tuple(data)) + + def test_flavor_list_long_no_extra_specs(self): + # use flavor with no extra specs for this test + flavor = compute_fakes.FakeFlavor.create_one_flavor( + attrs={"extra_specs": {}}) + self.data = (( + flavor.id, + flavor.name, + flavor.ram, + flavor.disk, + flavor.ephemeral, + flavor.vcpus, + flavor.is_public, + ),) + self.data_long = (self.data[0] + ( + flavor.swap, + flavor.rxtx_factor, + format_columns.DictColumn(flavor.extra_specs) + ),) + self.api_mock.side_effect = [[flavor], [], ] + + self.sdk_client.flavors = self.api_mock + self.sdk_client.fetch_flavor_extra_specs = mock.Mock(return_value=None) + + arglist = [ + '--long', + ] + verifylist = [ + ('long', True), + ] + + 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 + kwargs = { + 'is_public': True, + } + + self.sdk_client.flavors.assert_called_with( + **kwargs + ) + self.sdk_client.fetch_flavor_extra_specs.assert_called_once_with( + flavor) self.assertEqual(self.columns_long, columns) self.assertCountEqual(self.data_long, tuple(data)) @@ -662,6 +718,7 @@ def test_flavor_list_min_disk_min_ram(self): self.sdk_client.flavors.assert_called_with( **kwargs ) + self.sdk_client.fetch_flavor_extra_specs.assert_not_called() self.assertEqual(self.columns, columns) self.assertEqual(tuple(self.data), tuple(data)) diff --git a/releasenotes/notes/story-2010343-b5eb4ed593f51d3f.yaml b/releasenotes/notes/story-2010343-b5eb4ed593f51d3f.yaml new file mode 100644 index 0000000000..5e92f2f3ea --- /dev/null +++ b/releasenotes/notes/story-2010343-b5eb4ed593f51d3f.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + The ``flavor list`` command will no longer attempt to fetch extra specs + unless they are actually required (by using the ``--long``) option. This + should significantly improve performance on clouds with a large number of + flavors. + [Story `2010343 `_] From 4592d2c78fc4c814b4b7a08d400eddcd1c25ad19 Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Fri, 15 Jul 2022 14:03:18 +0300 Subject: [PATCH 031/595] Fix server evacuate command I don't think it has ever worked in fact.. The server.evacuate() in novaclient returns not a Server() instance, but a TupleWithMeta() object, that has no .to_dict() method [0] used in _prep_server_detail() function. [0] https://review.opendev.org/c/openstack/python-novaclient/+/276639 Change-Id: I22f9ac072eb1b0bfd1263a256e9bea4500c03290 Story: 2010204 Task: 45928 --- openstackclient/compute/v2/server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index b02ee6ff7f..81d50b25c1 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -3335,7 +3335,7 @@ def _show_progress(progress): server = utils.find_resource( compute_client.servers, parsed_args.server) - server = server.evacuate(**kwargs) + server.evacuate(**kwargs) if parsed_args.wait: if utils.wait_for_status( @@ -3350,7 +3350,7 @@ def _show_progress(progress): raise SystemExit details = _prep_server_detail( - compute_client, image_client, server, refresh=False) + compute_client, image_client, server, refresh=True) return zip(*sorted(details.items())) From 47e667e71d997ad4a7b0dd86bf462f746c964b54 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 23 Sep 2022 17:03:53 +0100 Subject: [PATCH 032/595] quota: Simplify logic used to list, show quotas This is prep work for some changes we're introducing in a later change. Change-Id: I27a59bc1d57e6815fb166fb99ea2af88f08b68a9 Signed-off-by: Stephen Finucane --- openstackclient/common/quota.py | 469 ++++++++++-------- .../tests/unit/common/test_quota.py | 23 - 2 files changed, 268 insertions(+), 224 deletions(-) diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 8477da9079..308ad4f447 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -25,7 +25,6 @@ from openstackclient.i18n import _ from openstackclient.network import common - LOG = logging.getLogger(__name__) # List the quota items, map the internal argument name to the option @@ -78,9 +77,17 @@ 'subnetpool': 'subnetpools', } -NETWORK_KEYS = ['floating_ips', 'networks', 'rbac_policies', 'routers', - 'ports', 'security_group_rules', 'security_groups', - 'subnet_pools', 'subnets'] +NETWORK_KEYS = [ + 'floating_ips', + 'networks', + 'rbac_policies', + 'routers', + 'ports', + 'security_group_rules', + 'security_groups', + 'subnet_pools', + 'subnets', +] def _xform_get_quota(data, value, keys): @@ -94,181 +101,151 @@ def _xform_get_quota(data, value, keys): return res -class BaseQuota(object): - def _get_project(self, parsed_args): - if parsed_args.project is not None: - identity_client = self.app.client_manager.identity - project = utils.find_resource( - identity_client.projects, - parsed_args.project, - ) - project_id = project.id - project_name = project.name - elif self.app.client_manager.auth_ref: - # Get the project from the current auth - project = self.app.client_manager.auth_ref - project_id = project.project_id - project_name = project.project_name +def get_project(app, project): + if project is not None: + identity_client = app.client_manager.identity + project = utils.find_resource( + identity_client.projects, + project, + ) + project_id = project.id + project_name = project.name + elif app.client_manager.auth_ref: + # Get the project from the current auth + project = app.client_manager.auth_ref + project_id = project.project_id + project_name = project.project_name + else: + project_id = None + project_name = None + + return { + 'id': project_id, + 'name': project_name, + } + + +def get_compute_quotas( + app, + project_id, + *, + quota_class=False, + detail=False, + default=False, +): + try: + client = app.client_manager.compute + if quota_class: + # NOTE(stephenfin): The 'project' argument here could be anything + # as the nova API doesn't care what you pass in. We only pass the + # project in to avoid weirding people out :) + quota = client.quota_classes.get(project_id) + elif default: + quota = client.quotas.defaults(project_id) + else: + quota = client.quotas.get(project_id, detail=detail) + except Exception as e: + if type(e).__name__ == 'EndpointNotFound': + return {} + raise + return quota._info + + +def get_volume_quotas( + app, + project_id, + *, + quota_class=False, + detail=False, + default=False, +): + try: + client = app.client_manager.volume + if quota_class: + quota = client.quota_classes.get(project_id) + elif default: + quota = client.quotas.defaults(project_id) + else: + quota = client.quotas.get(project_id, usage=detail) + except Exception as e: + if type(e).__name__ == 'EndpointNotFound': + return {} else: - project = None - project_id = None - project_name = None - project_info = {} - project_info['id'] = project_id - project_info['name'] = project_name - return project_info - - def get_compute_quota(self, client, parsed_args): - quota_class = ( - parsed_args.quota_class if 'quota_class' in parsed_args else False) - detail = parsed_args.detail if 'detail' in parsed_args else False - default = parsed_args.default if 'default' in parsed_args else False - try: - if quota_class: - quota = client.quota_classes.get(parsed_args.project) - else: - project_info = self._get_project(parsed_args) - project = project_info['id'] - if default: - quota = client.quotas.defaults(project) - else: - quota = client.quotas.get(project, detail=detail) - except Exception as e: - if type(e).__name__ == 'EndpointNotFound': - return {} - else: - raise - return quota._info - - def get_volume_quota(self, client, parsed_args): - quota_class = ( - parsed_args.quota_class if 'quota_class' in parsed_args else False) - detail = parsed_args.detail if 'detail' in parsed_args else False - default = parsed_args.default if 'default' in parsed_args else False - try: - if quota_class: - quota = client.quota_classes.get(parsed_args.project) - else: - project_info = self._get_project(parsed_args) - project = project_info['id'] - if default: - quota = client.quotas.defaults(project) - else: - quota = client.quotas.get(project, usage=detail) - except Exception as e: - if type(e).__name__ == 'EndpointNotFound': - return {} - else: - raise - return quota._info - - def _network_quota_to_dict(self, network_quota): + raise + return quota._info + + +def get_network_quotas( + app, + project_id, + *, + quota_class=False, + detail=False, + default=False, +): + def _network_quota_to_dict(network_quota, detail=False): if type(network_quota) is not dict: dict_quota = network_quota.to_dict() else: dict_quota = network_quota - return {k: v for k, v in dict_quota.items() if v is not None} - def get_network_quota(self, parsed_args): - quota_class = ( - parsed_args.quota_class if 'quota_class' in parsed_args else False) - detail = parsed_args.detail if 'detail' in parsed_args else False - default = parsed_args.default if 'default' in parsed_args else False - if quota_class: - return {} - if self.app.client_manager.is_network_endpoint_enabled(): - project_info = self._get_project(parsed_args) - project = project_info['id'] - client = self.app.client_manager.network - if default: - network_quota = client.get_quota_default(project) - network_quota = self._network_quota_to_dict(network_quota) - else: - network_quota = client.get_quota(project, - details=detail) - network_quota = self._network_quota_to_dict(network_quota) - if detail: - # NOTE(slaweq): Neutron returns values with key "used" but - # Nova for example returns same data with key "in_use" - # instead. - # Because of that we need to convert Neutron key to - # the same as is returned from Nova to make result - # more consistent - for key, values in network_quota.items(): - if type(values) is dict and "used" in values: - values['in_use'] = values.pop("used") - network_quota[key] = values - return network_quota - else: - return {} + result = {} + + for key, values in dict_quota.items(): + if values is None: + continue + + # NOTE(slaweq): Neutron returns values with key "used" but Nova for + # example returns same data with key "in_use" instead. Because of + # that we need to convert Neutron key to the same as is returned + # from Nova to make result more consistent + if isinstance(values, dict) and 'used' in values: + values['in_use'] = values.pop("used") + + result[key] = values + return result -class ListQuota(command.Lister, BaseQuota): + # neutron doesn't have the concept of quota classes and if we're using + # nova-network we already fetched this + if quota_class: + return {} + + # we have nothing to return if we are not using neutron + if not app.client_manager.is_network_endpoint_enabled(): + return {} + + client = app.client_manager.network + if default: + network_quota = client.get_quota_default(project_id) + network_quota = _network_quota_to_dict(network_quota) + else: + network_quota = client.get_quota(project_id, details=detail) + network_quota = _network_quota_to_dict(network_quota, detail=detail) + return network_quota + + +class ListQuota(command.Lister): _description = _( "List quotas for all projects with non-default quota values or " "list detailed quota information for requested project" ) - def _get_detailed_quotas(self, parsed_args): - columns = ( - 'resource', - 'in_use', - 'reserved', - 'limit' - ) - column_headers = ( - 'Resource', - 'In Use', - 'Reserved', - 'Limit' - ) - quotas = {} - if parsed_args.compute: - quotas.update( - self.get_compute_quota( - self.app.client_manager.compute, - parsed_args, - ) - ) - if parsed_args.network: - quotas.update(self.get_network_quota(parsed_args)) - if parsed_args.volume: - quotas.update( - self.get_volume_quota( - self.app.client_manager.volume, - parsed_args, - ), - ) - - result = [] - for resource, values in quotas.items(): - # NOTE(slaweq): there is no detailed quotas info for some resources - # and it shouldn't be displayed here - if type(values) is dict: - result.append({ - 'resource': resource, - 'in_use': values.get('in_use'), - 'reserved': values.get('reserved'), - 'limit': values.get('limit') - }) - return (column_headers, - (utils.get_dict_properties( - s, columns, - ) for s in result)) - def get_parser(self, prog_name): - parser = super(ListQuota, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( '--project', metavar='', help=_('List quotas for this project (name or ID)'), ) + # TODO(stephenfin): This doesn't belong here. We should put it into the + # 'quota show' command and deprecate this. parser.add_argument( '--detail', dest='detail', action='store_true', default=False, - help=_('Show details about quotas usage') + help=_('Show details about quotas usage'), ) option = parser.add_mutually_exclusive_group(required=True) option.add_argument( @@ -291,6 +268,69 @@ def get_parser(self, prog_name): ) return parser + def _get_detailed_quotas(self, parsed_args): + project_info = get_project(self.app, parsed_args.project) + project = project_info['id'] + + quotas = {} + + if parsed_args.compute: + quotas.update( + get_compute_quotas( + self.app, + project, + detail=parsed_args.detail, + ) + ) + + if parsed_args.network: + quotas.update( + get_network_quotas( + self.app, + project, + detail=parsed_args.detail, + ) + ) + + if parsed_args.volume: + quotas.update( + get_volume_quotas( + self.app, + parsed_args, + detail=parsed_args.detail, + ), + ) + + result = [] + for resource, values in quotas.items(): + # NOTE(slaweq): there is no detailed quotas info for some resources + # and it shouldn't be displayed here + if isinstance(values, dict): + result.append({ + 'resource': resource, + 'in_use': values.get('in_use'), + 'reserved': values.get('reserved'), + 'limit': values.get('limit'), + }) + + columns = ( + 'resource', + 'in_use', + 'reserved', + 'limit', + ) + column_headers = ( + 'Resource', + 'In Use', + 'Reserved', + 'Limit', + ) + + return ( + column_headers, + (utils.get_dict_properties(s, columns) for s in result), + ) + def take_action(self, parsed_args): result = [] project_ids = [] @@ -308,6 +348,7 @@ def take_action(self, parsed_args): if parsed_args.compute: if parsed_args.detail: return self._get_detailed_quotas(parsed_args) + compute_client = self.app.client_manager.compute for p in project_ids: try: @@ -365,14 +406,15 @@ def take_action(self, parsed_args): 'Server Groups', 'Server Group Members', ) - return (column_headers, - (utils.get_dict_properties( - s, columns, - ) for s in result)) + return ( + column_headers, + (utils.get_dict_properties(s, columns) for s in result), + ) if parsed_args.volume: if parsed_args.detail: return self._get_detailed_quotas(parsed_args) + volume_client = self.app.client_manager.volume for p in project_ids: try: @@ -417,14 +459,16 @@ def take_action(self, parsed_args): 'Snapshots', 'Volumes', ) - return (column_headers, - (utils.get_dict_properties( - s, columns, - ) for s in result)) + + return ( + column_headers, + (utils.get_dict_properties(s, columns) for s in result), + ) if parsed_args.network: if parsed_args.detail: return self._get_detailed_quotas(parsed_args) + client = self.app.client_manager.network for p in project_ids: try: @@ -475,10 +519,11 @@ def take_action(self, parsed_args): 'Subnets', 'Subnet Pools' ) - return (column_headers, - (utils.get_dict_properties( - s, columns, - ) for s in result)) + + return ( + column_headers, + (utils.get_dict_properties(s, columns) for s in result), + ) return ((), ()) @@ -545,14 +590,16 @@ def get_parser(self, prog_name): parser.add_argument( '--force', action='store_true', - help=_('Force quota update (only supported by compute and ' - 'network)') + help=_( + 'Force quota update (only supported by compute and network)' + ), ) parser.add_argument( '--check-limit', action='store_true', - help=_('Check quota limit when updating (only supported by ' - 'network)') + help=_( + 'Check quota limit when updating (only supported by network)' + ), ) return parser @@ -631,14 +678,16 @@ def take_action(self, parsed_args): **network_kwargs) -class ShowQuota(command.ShowOne, BaseQuota): +class ShowQuota(command.ShowOne): _description = _( - "Show quotas for project or class. Specify " - "``--os-compute-api-version 2.50`` or higher to see ``server-groups`` " - "and ``server-group-members`` output for a given quota class.") + "Show quotas for project or class. " + "Specify ``--os-compute-api-version 2.50`` or higher to see " + "``server-groups`` and ``server-group-members`` output for a given " + "quota class." + ) def get_parser(self, prog_name): - parser = super(ShowQuota, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( 'project', metavar='', @@ -658,29 +707,40 @@ def get_parser(self, prog_name): dest='default', action='store_true', default=False, - help=_('Show default quotas for ') + help=_('Show default quotas for '), ) return parser def take_action(self, parsed_args): + project = parsed_args.project - compute_client = self.app.client_manager.compute - volume_client = self.app.client_manager.volume - # NOTE(dtroyer): These quota API calls do not validate the project - # or class arguments and return what appears to be - # the default quota values if the project or class - # does not exist. If this is determined to be the - # intended behaviour of the API we will validate - # the argument with Identity ourselves later. - compute_quota_info = self.get_compute_quota(compute_client, - parsed_args) - volume_quota_info = self.get_volume_quota(volume_client, - parsed_args) - network_quota_info = self.get_network_quota(parsed_args) - # NOTE(reedip): Remove the below check once requirement for - # Openstack SDK is fixed to version 0.9.12 and above - if type(network_quota_info) is not dict: - network_quota_info = network_quota_info.to_dict() + if not parsed_args.quota_class: + project_info = get_project(self.app, parsed_args.project) + project = project_info['id'] + + # NOTE(dtroyer): These quota API calls do not validate the project or + # class arguments and return what appears to be the default quota + # values if the project or class does not exist. If this is determined + # to be the intended behaviour of the API we will validate the argument + # with Identity ourselves later. + compute_quota_info = get_compute_quotas( + self.app, + project, + quota_class=parsed_args.quota_class, + default=parsed_args.default, + ) + volume_quota_info = get_volume_quotas( + self.app, + project, + quota_class=parsed_args.quota_class, + default=parsed_args.default, + ) + network_quota_info = get_network_quotas( + self.app, + project, + quota_class=parsed_args.quota_class, + default=parsed_args.default, + ) info = {} info.update(compute_quota_info) @@ -693,20 +753,27 @@ def take_action(self, parsed_args): # neutron is enabled, quotas of these three resources # in nova will be replaced by neutron's. for k, v in itertools.chain( - COMPUTE_QUOTAS.items(), NOVA_NETWORK_QUOTAS.items(), - VOLUME_QUOTAS.items(), NETWORK_QUOTAS.items()): + COMPUTE_QUOTAS.items(), + NOVA_NETWORK_QUOTAS.items(), + VOLUME_QUOTAS.items(), + NETWORK_QUOTAS.items(), + ): if not k == v and info.get(k) is not None: info[v] = info[k] info.pop(k) - # Handle project ID special as it only appears in output - if 'id' in info: + # Remove the 'location' field for resources from openstacksdk + if 'location' in info: + del info['location'] + + # Handle class or project ID specially as they only appear in output + if parsed_args.quota_class: + info.pop('id', None) + elif 'id' in info: info['project'] = info.pop('id') if 'project_id' in info: del info['project_id'] - project_info = self._get_project(parsed_args) - project_name = project_info['name'] - info['project_name'] = project_name + info['project_name'] = project_info['name'] return zip(*sorted(info.items())) diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index 580072086d..663b62eab0 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -1166,29 +1166,6 @@ def test_quota_show_no_project(self): ) self.assertNotCalled(self.network.get_quota_default) - def test_network_quota_show_remove_empty(self): - arglist = [ - self.projects[0].name, - ] - verifylist = [ - ('project', self.projects[0].name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # First check that all regular values are returned - result = self.cmd.get_network_quota(parsed_args) - self.assertEqual(len(network_fakes.QUOTA), len(result)) - - # set 1 of the values to None, and verify it is not returned - orig_get_quota = self.network.get_quota - network_quotas = copy.copy(network_fakes.QUOTA) - network_quotas['healthmonitor'] = None - self.network.get_quota = mock.Mock(return_value=network_quotas) - result = self.cmd.get_network_quota(parsed_args) - self.assertEqual(len(network_fakes.QUOTA) - 1, len(result)) - # Go back to default mock - self.network.get_quota = orig_get_quota - class TestQuotaDelete(TestQuota): """Test cases for quota delete command""" From 04e68e0d5a49be93f79d6d71821ab8cd0b0ce589 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 23 Sep 2022 18:00:34 +0100 Subject: [PATCH 033/595] quota: Add 'quota show --usage' option Provide an more sane way to get usage information for a particular project's quotas. This requires using the 'Lister' command type since the 'ShowOne' command type only allows for simple key-value pair output. We also add a note indicating that the '' argument is optional. Change-Id: Ic7342cf08f024cc690049414c5eef5b9a7594677 Signed-off-by: Stephen Finucane --- doc/source/cli/data/cinder.csv | 2 +- openstackclient/common/quota.py | 91 +++++++++++++++---- .../tests/functional/common/test_quota.py | 4 + .../tests/unit/common/test_quota.py | 68 +++++++++++--- ...ta-show-usage-option-19b1f59fb5f3498f.yaml | 18 ++++ 5 files changed, 152 insertions(+), 31 deletions(-) create mode 100644 releasenotes/notes/quota-show-usage-option-19b1f59fb5f3498f.yaml diff --git a/doc/source/cli/data/cinder.csv b/doc/source/cli/data/cinder.csv index 9fe6c9d685..8b25d3fd28 100644 --- a/doc/source/cli/data/cinder.csv +++ b/doc/source/cli/data/cinder.csv @@ -94,7 +94,7 @@ quota-defaults,quota show --default,Lists default quotas for a tenant. quota-delete,quota delete --volume,Delete the quotas for a tenant. quota-show,quota show,Lists quotas for a tenant. quota-update,quota set,Updates quotas for a tenant. -quota-usage,quota list --detail,Lists quota usage for a tenant. +quota-usage,quota show --usage,Lists quota usage for a tenant. rate-limits,limits show --rate,Lists rate limits for a user. readonly-mode-update,volume set --read-only-mode | --read-write-mode,Updates volume read-only access-mode flag. rename,volume set --name,Renames a volume. diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 308ad4f447..b1491700b1 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -233,19 +233,26 @@ class ListQuota(command.Lister): def get_parser(self, prog_name): parser = super().get_parser(prog_name) + # TODO(stephenfin): Remove in OSC 8.0 parser.add_argument( '--project', metavar='', - help=_('List quotas for this project (name or ID)'), + help=_( + "**Deprecated** List quotas for this project " + "(name or ID). " + "Use 'quota show' instead." + ), ) - # TODO(stephenfin): This doesn't belong here. We should put it into the - # 'quota show' command and deprecate this. + # TODO(stephenfin): Remove in OSC 8.0 parser.add_argument( '--detail', dest='detail', action='store_true', default=False, - help=_('Show details about quotas usage'), + help=_( + "**Deprecated** Show details about quotas usage. " + "Use 'quota show --usage' instead." + ), ) option = parser.add_mutually_exclusive_group(required=True) option.add_argument( @@ -332,6 +339,19 @@ def _get_detailed_quotas(self, parsed_args): ) def take_action(self, parsed_args): + if parsed_args.detail: + msg = _( + "The --detail option has been deprecated. " + "Use 'openstack quota show --usage' instead." + ) + self.log.warning(msg) + elif parsed_args.project: # elif to avoid being too noisy + msg = _( + "The --project option has been deprecated. " + "Use 'openstack quota show' instead." + ) + self.log.warning(msg) + result = [] project_ids = [] if parsed_args.project is None: @@ -678,7 +698,7 @@ def take_action(self, parsed_args): **network_kwargs) -class ShowQuota(command.ShowOne): +class ShowQuota(command.Lister): _description = _( "Show quotas for project or class. " "Specify ``--os-compute-api-version 2.50`` or higher to see " @@ -692,7 +712,10 @@ def get_parser(self, prog_name): 'project', metavar='', nargs='?', - help=_('Show quotas for this project or class (name or ID)'), + help=_( + 'Show quotas for this project or class (name or ID) ' + '(defaults to current project)' + ), ) type_group = parser.add_mutually_exclusive_group() type_group.add_argument( @@ -709,6 +732,13 @@ def get_parser(self, prog_name): default=False, help=_('Show default quotas for '), ) + type_group.add_argument( + '--usage', + dest='usage', + action='store_true', + default=False, + help=_('Show details about quotas usage'), + ) return parser def take_action(self, parsed_args): @@ -726,18 +756,21 @@ def take_action(self, parsed_args): compute_quota_info = get_compute_quotas( self.app, project, + detail=parsed_args.usage, quota_class=parsed_args.quota_class, default=parsed_args.default, ) volume_quota_info = get_volume_quotas( self.app, project, + detail=parsed_args.usage, quota_class=parsed_args.quota_class, default=parsed_args.default, ) network_quota_info = get_network_quotas( self.app, project, + detail=parsed_args.usage, quota_class=parsed_args.quota_class, default=parsed_args.default, ) @@ -762,20 +795,46 @@ def take_action(self, parsed_args): info[v] = info[k] info.pop(k) + # Remove the 'id' field since it's not very useful + if 'id' in info: + del info['id'] + # Remove the 'location' field for resources from openstacksdk if 'location' in info: del info['location'] - # Handle class or project ID specially as they only appear in output - if parsed_args.quota_class: - info.pop('id', None) - elif 'id' in info: - info['project'] = info.pop('id') - if 'project_id' in info: - del info['project_id'] - info['project_name'] = project_info['name'] - - return zip(*sorted(info.items())) + if not parsed_args.usage: + result = [ + {'resource': k, 'limit': v} for k, v in info.items() + ] + else: + result = [ + {'resource': k, **v} for k, v in info.items() + ] + + columns = ( + 'resource', + 'limit', + ) + column_headers = ( + 'Resource', + 'Limit', + ) + + if parsed_args.usage: + columns += ( + 'in_use', + 'reserved', + ) + column_headers += ( + 'In Use', + 'Reserved', + ) + + return ( + column_headers, + (utils.get_dict_properties(s, columns) for s in result), + ) class DeleteQuota(command.Command): diff --git a/openstackclient/tests/functional/common/test_quota.py b/openstackclient/tests/functional/common/test_quota.py index 783294df15..08ec626f86 100644 --- a/openstackclient/tests/functional/common/test_quota.py +++ b/openstackclient/tests/functional/common/test_quota.py @@ -114,6 +114,7 @@ def test_quota_set_project(self): cmd_output = json.loads(self.openstack( 'quota show -f json ' + self.PROJECT_NAME )) + cmd_output = {x['Resource']: x['Limit'] for x in cmd_output} self.assertIsNotNone(cmd_output) self.assertEqual( 31, @@ -136,6 +137,7 @@ def test_quota_set_project(self): self.assertIsNotNone(cmd_output) # We don't necessarily know the default quotas, we're checking the # returned attributes + cmd_output = {x['Resource']: x['Limit'] for x in cmd_output} self.assertTrue(cmd_output["cores"] >= 0) self.assertTrue(cmd_output["backups"] >= 0) if self.haz_network: @@ -150,6 +152,7 @@ def test_quota_set_class(self): 'quota show -f json --class default' )) self.assertIsNotNone(cmd_output) + cmd_output = {x['Resource']: x['Limit'] for x in cmd_output} self.assertEqual( 33, cmd_output["key-pairs"], @@ -166,6 +169,7 @@ def test_quota_set_class(self): self.assertIsNotNone(cmd_output) # We don't necessarily know the default quotas, we're checking the # returned attributes + cmd_output = {x['Resource']: x['Limit'] for x in cmd_output} self.assertTrue(cmd_output["key-pairs"] >= 0) self.assertTrue(cmd_output["snapshots"] >= 0) diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index 663b62eab0..53aab5f246 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -1094,17 +1094,20 @@ def test_quota_show(self): self.cmd.take_action(parsed_args) self.compute_quotas_mock.get.assert_called_once_with( - self.projects[0].id, detail=False + self.projects[0].id, + detail=False, ) self.volume_quotas_mock.get.assert_called_once_with( - self.projects[0].id, usage=False + self.projects[0].id, + usage=False, ) self.network.get_quota.assert_called_once_with( - self.projects[0].id, details=False + self.projects[0].id, + details=False, ) self.assertNotCalled(self.network.get_quota_default) - def test_quota_show_with_default(self): + def test_quota_show__with_default(self): arglist = [ '--default', self.projects[0].name, @@ -1128,30 +1131,67 @@ def test_quota_show_with_default(self): ) self.assertNotCalled(self.network.get_quota) - def test_quota_show_with_class(self): + def test_quota_show__with_class(self): arglist = [ '--class', - self.projects[0].name, + 'default', ] verifylist = [ ('quota_class', True), - ('project', self.projects[0].name), + ('project', 'default'), # project is actually a class here ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) - self.compute_quotas_class_mock.get.assert_called_once_with( + self.compute_quotas_class_mock.get.assert_called_once_with('default') + self.volume_quotas_class_mock.get.assert_called_once_with('default') + # neutron doesn't have the concept of quota classes + self.assertNotCalled(self.network.get_quota) + self.assertNotCalled(self.network.get_quota_default) + + def test_quota_show__with_usage(self): + # update mocks to return detailed quota instead + self.compute_quota = \ + compute_fakes.FakeQuota.create_one_comp_detailed_quota() + self.compute_quotas_mock.get.return_value = self.compute_quota + self.volume_quota = \ + volume_fakes.FakeQuota.create_one_detailed_quota() + self.volume_quotas_mock.get.return_value = self.volume_quota + self.network.get_quota.return_value = \ + network_fakes.FakeQuota.create_one_net_detailed_quota() + + arglist = [ + '--usage', self.projects[0].name, + ] + verifylist = [ + ('usage', True), + ('project', self.projects[0].name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.compute_quotas_mock.get.assert_called_once_with( + self.projects[0].id, + detail=True, ) - self.volume_quotas_class_mock.get.assert_called_once_with( - self.projects[0].name, + self.volume_quotas_mock.get.assert_called_once_with( + self.projects[0].id, + usage=True, + ) + self.network.get_quota.assert_called_once_with( + self.projects[0].id, + details=True, ) - self.assertNotCalled(self.network.get_quota) - self.assertNotCalled(self.network.get_quota_default) - def test_quota_show_no_project(self): - parsed_args = self.check_parser(self.cmd, [], []) + def test_quota_show__no_project(self): + arglist = [] + verifylist = [ + ('project', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) diff --git a/releasenotes/notes/quota-show-usage-option-19b1f59fb5f3498f.yaml b/releasenotes/notes/quota-show-usage-option-19b1f59fb5f3498f.yaml new file mode 100644 index 0000000000..b540edf4f7 --- /dev/null +++ b/releasenotes/notes/quota-show-usage-option-19b1f59fb5f3498f.yaml @@ -0,0 +1,18 @@ +--- +features: + - | + The ``quota show`` command now supports a ``--usage`` option. When + provided, this will result in the command returning usage information for + each quota. This replaces the ``quota list --detail`` command which is now + deprecated for removal. +deprecations: + - | + The ``--detail`` option for the ``quota list`` command has been deprecated + for removal. When used without the ``--detail`` option, the ``quota list`` + command returned quota information for multiple projects yet when used with + this option it only returned (detailed) quota information for a single + project. This detailed quota information is now available via the + ``quota show --usage`` command. + - | + The ``--project`` option for the ``quota list`` command has been deprecated + for removal. Use the ``quota show`` command instead. From 00e7019022585bc2be9aeb55eb40b1d04776ec22 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 23 Sep 2022 18:16:53 +0100 Subject: [PATCH 034/595] quota: Allow showing project-specific quotas Add '--compute', '--network' and '--volume' options to the 'quota show' command, along with a default '--all' option, allowing us to restrict quotas shown to an individual service. Change-Id: I122b765df01887b8d916ee6567ffb7768fcb4392 Signed-off-by: Stephen Finucane --- openstackclient/common/quota.py | 93 +++++++++++++------ .../tests/unit/common/test_quota.py | 62 +++++++++++++ ...show-service-options-ba48d6eca8ffc4f9.yaml | 5 + 3 files changed, 134 insertions(+), 26 deletions(-) create mode 100644 releasenotes/notes/quota-show-service-options-ba48d6eca8ffc4f9.yaml diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index b1491700b1..0504b15298 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -739,6 +739,40 @@ def get_parser(self, prog_name): default=False, help=_('Show details about quotas usage'), ) + service_group = parser.add_mutually_exclusive_group() + service_group.add_argument( + '--all', + action='store_const', + const='all', + dest='service', + default='all', + help=_('Show quotas for all services'), + ) + service_group.add_argument( + '--compute', + action='store_const', + const='compute', + dest='service', + default='all', + help=_('Show compute quota'), + ) + service_group.add_argument( + '--volume', + action='store_const', + const='volume', + dest='service', + default='all', + help=_('Show volume quota'), + ) + service_group.add_argument( + '--network', + action='store_const', + const='network', + dest='service', + default='all', + help=_('Show network quota'), + ) + return parser def take_action(self, parsed_args): @@ -748,32 +782,39 @@ def take_action(self, parsed_args): project_info = get_project(self.app, parsed_args.project) project = project_info['id'] - # NOTE(dtroyer): These quota API calls do not validate the project or - # class arguments and return what appears to be the default quota - # values if the project or class does not exist. If this is determined - # to be the intended behaviour of the API we will validate the argument - # with Identity ourselves later. - compute_quota_info = get_compute_quotas( - self.app, - project, - detail=parsed_args.usage, - quota_class=parsed_args.quota_class, - default=parsed_args.default, - ) - volume_quota_info = get_volume_quotas( - self.app, - project, - detail=parsed_args.usage, - quota_class=parsed_args.quota_class, - default=parsed_args.default, - ) - network_quota_info = get_network_quotas( - self.app, - project, - detail=parsed_args.usage, - quota_class=parsed_args.quota_class, - default=parsed_args.default, - ) + compute_quota_info = {} + volume_quota_info = {} + network_quota_info = {} + + # NOTE(stephenfin): These quota API calls do not validate the project + # or class arguments and return what appears to be the default quota + # values if the project or class does not exist. This is expected + # behavior. However, we have already checked for the presence of the + # project above so it shouldn't be an issue. + if parsed_args.service in {'all', 'compute'}: + compute_quota_info = get_compute_quotas( + self.app, + project, + detail=parsed_args.usage, + quota_class=parsed_args.quota_class, + default=parsed_args.default, + ) + if parsed_args.service in {'all', 'volume'}: + volume_quota_info = get_volume_quotas( + self.app, + project, + detail=parsed_args.usage, + quota_class=parsed_args.quota_class, + default=parsed_args.default, + ) + if parsed_args.service in {'all', 'network'}: + network_quota_info = get_network_quotas( + self.app, + project, + detail=parsed_args.usage, + quota_class=parsed_args.quota_class, + default=parsed_args.default, + ) info = {} info.update(compute_quota_info) diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index 53aab5f246..0900200e96 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -1087,6 +1087,7 @@ def test_quota_show(self): self.projects[0].name, ] verifylist = [ + ('service', 'all'), ('project', self.projects[0].name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1107,6 +1108,67 @@ def test_quota_show(self): ) self.assertNotCalled(self.network.get_quota_default) + def test_quota_show__with_compute(self): + arglist = [ + '--compute', + self.projects[0].name, + ] + verifylist = [ + ('service', 'compute'), + ('project', self.projects[0].name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.compute_quotas_mock.get.assert_called_once_with( + self.projects[0].id, + detail=False, + ) + self.volume_quotas_mock.get.assert_not_called() + self.network.get_quota.assert_not_called() + + def test_quota_show__with_volume(self): + arglist = [ + '--volume', + self.projects[0].name, + ] + verifylist = [ + ('service', 'volume'), + ('project', self.projects[0].name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.compute_quotas_mock.get.assert_not_called() + self.volume_quotas_mock.get.assert_called_once_with( + self.projects[0].id, + usage=False, + ) + self.network.get_quota.assert_not_called() + + def test_quota_show__with_network(self): + arglist = [ + '--network', + self.projects[0].name, + ] + verifylist = [ + ('service', 'network'), + ('project', self.projects[0].name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.compute_quotas_mock.get.assert_not_called() + self.volume_quotas_mock.get.assert_not_called() + self.network.get_quota.assert_called_once_with( + self.projects[0].id, + details=False, + ) + self.assertNotCalled(self.network.get_quota_default) + def test_quota_show__with_default(self): arglist = [ '--default', diff --git a/releasenotes/notes/quota-show-service-options-ba48d6eca8ffc4f9.yaml b/releasenotes/notes/quota-show-service-options-ba48d6eca8ffc4f9.yaml new file mode 100644 index 0000000000..b171d1931d --- /dev/null +++ b/releasenotes/notes/quota-show-service-options-ba48d6eca8ffc4f9.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + The ``quota show`` command now allows you to show quotas for a specific + service using the ``--compute``, ``--volume``, or ``--network`` options. From b62021260c9c2d372ea635a2a7f0608b7c8472e1 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 23 Sep 2022 18:21:56 +0100 Subject: [PATCH 035/595] quota: Deprecate 'quota show --class', 'quota set --class' arguments This doesn't do anything special in nova and cinder and is not supported in neutron. For the 'quota show' command, people should use the '--default' argument instead. Change-Id: I0dd38e5cb252a01d5817ed168be040b21b35e348 Signed-off-by: Stephen Finucane --- openstackclient/common/quota.py | 40 +++++++++++++++++-- ...-quota-class-options-ba33a45caedbdf3e.yaml | 11 +++++ 2 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/deprecated-quota-class-options-ba33a45caedbdf3e.yaml diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 0504b15298..6739c51b5a 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -585,14 +585,20 @@ def get_parser(self, prog_name): parser.add_argument( 'project', metavar='', - help=_('Set quotas for this project or class (name/ID)'), + help=_('Set quotas for this project or class (name or ID)'), ) + # TODO(stephenfin): Remove in OSC 8.0 parser.add_argument( '--class', dest='quota_class', action='store_true', default=False, - help=_('Set quotas for '), + help=_( + '**Deprecated** Set quotas for . ' + 'Deprecated as quota classes were never fully implemented ' + 'and only the default class is supported. ' + '(compute and volume only)' + ), ) for k, v, h in self._build_options_list(): parser.add_argument( @@ -624,6 +630,15 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): + if parsed_args.quota_class: + msg = _( + "The '--class' option has been deprecated. Quota classes were " + "never fully implemented and the compute and volume services " + "only support a single 'default' quota class while the " + "network service does not support quota classes at all. " + "Please use 'openstack quota show --default' instead." + ) + self.log.warning(msg) identity_client = self.app.client_manager.identity compute_client = self.app.client_manager.compute @@ -718,12 +733,20 @@ def get_parser(self, prog_name): ), ) type_group = parser.add_mutually_exclusive_group() + # TODO(stephenfin): Remove in OSC 8.0 type_group.add_argument( '--class', dest='quota_class', action='store_true', default=False, - help=_('Show quotas for '), + help=_( + '**Deprecated** Show quotas for . ' + 'Deprecated as quota classes were never fully implemented ' + 'and only the default class is supported. ' + 'Use --default instead which is also supported by the network ' + 'service. ' + '(compute and volume only)' + ), ) type_group.add_argument( '--default', @@ -778,7 +801,16 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): project = parsed_args.project - if not parsed_args.quota_class: + if parsed_args.quota_class: + msg = _( + "The '--class' option has been deprecated. Quota classes were " + "never fully implemented and the compute and volume services " + "only support a single 'default' quota class while the " + "network service does not support quota classes at all. " + "Please use 'openstack quota show --default' instead." + ) + self.log.warning(msg) + else: project_info = get_project(self.app, parsed_args.project) project = project_info['id'] diff --git a/releasenotes/notes/deprecated-quota-class-options-ba33a45caedbdf3e.yaml b/releasenotes/notes/deprecated-quota-class-options-ba33a45caedbdf3e.yaml new file mode 100644 index 0000000000..d1800f7a02 --- /dev/null +++ b/releasenotes/notes/deprecated-quota-class-options-ba33a45caedbdf3e.yaml @@ -0,0 +1,11 @@ +--- +deprecations: + - | + The ``--class`` options of the ``quota show`` and ``quota set`` commands + are now deprecated. Quota classes were never fully implemented and the + compute and volume services only support a single ``default`` quota class + while the network service does not support quota classes at all. The + default quotas can be changed on a deployment-wide basis via configuration + and can be inspected using the ``openstack quota show --default`` command. + Quotas can still be set on a project-specific basis using the ``quota set`` + command. From 1ff839da76545f1c3e1a7df48eb8fb91ebf45abb Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 29 Sep 2022 18:26:31 +0100 Subject: [PATCH 036/595] quota: Trivial style fixups Change-Id: I4522b54676033dced2b47477238ceb551e11d04a Signed-off-by: Stephen Finucane --- openstackclient/common/quota.py | 78 +++++++++++++++++---------------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 6739c51b5a..10f40a75d4 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -313,12 +313,14 @@ def _get_detailed_quotas(self, parsed_args): # NOTE(slaweq): there is no detailed quotas info for some resources # and it shouldn't be displayed here if isinstance(values, dict): - result.append({ - 'resource': resource, - 'in_use': values.get('in_use'), - 'reserved': values.get('reserved'), - 'limit': values.get('limit'), - }) + result.append( + { + 'resource': resource, + 'in_use': values.get('in_use'), + 'reserved': values.get('reserved'), + 'limit': values.get('limit'), + } + ) columns = ( 'resource', @@ -375,8 +377,9 @@ def take_action(self, parsed_args): data = compute_client.quotas.get(p) except Exception as ex: if ( - type(ex).__name__ == 'NotFound' or - ex.http_status >= 400 and ex.http_status <= 499 + type(ex).__name__ == 'NotFound' + or ex.http_status >= 400 + and ex.http_status <= 499 ): # Project not found, move on to next one LOG.warning("Project %s not found: %s" % (p, ex)) @@ -537,7 +540,7 @@ def take_action(self, parsed_args): 'Security Groups', 'Security Group Rules', 'Subnets', - 'Subnet Pools' + 'Subnet Pools', ) return ( @@ -554,10 +557,13 @@ class SetQuota(common.NetDetectionMixin, command.Command): def _build_options_list(self): help_fmt = _('New value for the %s quota') # Compute and volume quota options are always the same - rets = [(k, v, help_fmt % v) for k, v in itertools.chain( - COMPUTE_QUOTAS.items(), - VOLUME_QUOTAS.items(), - )] + rets = [ + (k, v, help_fmt % v) + for k, v in itertools.chain( + COMPUTE_QUOTAS.items(), + VOLUME_QUOTAS.items(), + ) + ] # For docs build, we want to produce helps for both neutron and # nova-network options. They overlap, so we have to figure out which # need to be tagged as specific to one network type or the other. @@ -574,10 +580,12 @@ def _build_options_list(self): rets.append((k, v, _help)) elif self.is_neutron: rets.extend( - [(k, v, help_fmt % v) for k, v in NETWORK_QUOTAS.items()]) + [(k, v, help_fmt % v) for k, v in NETWORK_QUOTAS.items()] + ) elif self.is_nova_network: rets.extend( - [(k, v, help_fmt % v) for k, v in NOVA_NETWORK_QUOTAS.items()]) + [(k, v, help_fmt % v) for k, v in NOVA_NETWORK_QUOTAS.items()] + ) return rets def get_parser(self, prog_name): @@ -656,8 +664,7 @@ def take_action(self, parsed_args): for k, v in VOLUME_QUOTAS.items(): value = getattr(parsed_args, k, None) if value is not None: - if (parsed_args.volume_type and - k in IMPACT_VOLUME_TYPE_QUOTAS): + if parsed_args.volume_type and k in IMPACT_VOLUME_TYPE_QUOTAS: k = k + '_%s' % parsed_args.volume_type volume_kwargs[k] = value @@ -682,35 +689,34 @@ def take_action(self, parsed_args): if compute_kwargs: compute_client.quota_classes.update( parsed_args.project, - **compute_kwargs) + **compute_kwargs, + ) if volume_kwargs: volume_client.quota_classes.update( parsed_args.project, - **volume_kwargs) + **volume_kwargs, + ) if network_kwargs: - sys.stderr.write("Network quotas are ignored since quota class" - " is not supported.") + sys.stderr.write( + "Network quotas are ignored since quota classes are not " + "supported." + ) else: project = utils.find_resource( identity_client.projects, parsed_args.project, ).id + if compute_kwargs: - compute_client.quotas.update( - project, - **compute_kwargs) + compute_client.quotas.update(project, **compute_kwargs) if volume_kwargs: - volume_client.quotas.update( - project, - **volume_kwargs) + volume_client.quotas.update(project, **volume_kwargs) if ( - network_kwargs and - self.app.client_manager.is_network_endpoint_enabled() + network_kwargs + and self.app.client_manager.is_network_endpoint_enabled() ): network_client = self.app.client_manager.network - network_client.update_quota( - project, - **network_kwargs) + network_client.update_quota(project, **network_kwargs) class ShowQuota(command.Lister): @@ -877,13 +883,9 @@ def take_action(self, parsed_args): del info['location'] if not parsed_args.usage: - result = [ - {'resource': k, 'limit': v} for k, v in info.items() - ] + result = [{'resource': k, 'limit': v} for k, v in info.items()] else: - result = [ - {'resource': k, **v} for k, v in info.items() - ] + result = [{'resource': k, **v} for k, v in info.items()] columns = ( 'resource', From e2940eea18e2ad5e40fbee63af7502aa1055938d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 30 Sep 2022 10:46:45 +0100 Subject: [PATCH 037/595] tests: Move fake clients to top of file These are important and they're easier to identify at the top of the file than nested deep inside. Change-Id: I1569c855fadfe4cdacf83ee07d08922bf3edefbc Signed-off-by: Stephen Finucane --- openstackclient/tests/unit/volume/v1/fakes.py | 110 +++++++------- openstackclient/tests/unit/volume/v2/fakes.py | 134 +++++++++--------- openstackclient/tests/unit/volume/v3/fakes.py | 2 +- 3 files changed, 123 insertions(+), 123 deletions(-) diff --git a/openstackclient/tests/unit/volume/v1/fakes.py b/openstackclient/tests/unit/volume/v1/fakes.py index 438a60ade5..c07af29914 100644 --- a/openstackclient/tests/unit/volume/v1/fakes.py +++ b/openstackclient/tests/unit/volume/v1/fakes.py @@ -23,6 +23,61 @@ from openstackclient.tests.unit import utils +class FakeImagev1Client: + + def __init__(self, **kwargs): + self.images = mock.Mock() + + +class FakeVolumev1Client: + + def __init__(self, **kwargs): + self.volumes = mock.Mock() + self.volumes.resource_class = fakes.FakeResource(None, {}) + self.services = mock.Mock() + self.services.resource_class = fakes.FakeResource(None, {}) + self.extensions = mock.Mock() + self.extensions.resource_class = fakes.FakeResource(None, {}) + self.qos_specs = mock.Mock() + self.qos_specs.resource_class = fakes.FakeResource(None, {}) + self.volume_types = mock.Mock() + self.volume_types.resource_class = fakes.FakeResource(None, {}) + self.volume_encryption_types = mock.Mock() + self.volume_encryption_types.resource_class = ( + fakes.FakeResource(None, {})) + self.transfers = mock.Mock() + self.transfers.resource_class = fakes.FakeResource(None, {}) + self.volume_snapshots = mock.Mock() + self.volume_snapshots.resource_class = fakes.FakeResource(None, {}) + self.backups = mock.Mock() + self.backups.resource_class = fakes.FakeResource(None, {}) + self.restores = mock.Mock() + self.restores.resource_class = fakes.FakeResource(None, {}) + self.auth_token = kwargs['token'] + self.management_url = kwargs['endpoint'] + + +class TestVolumev1(utils.TestCommand): + + def setUp(self): + super().setUp() + + self.app.client_manager.volume = FakeVolumev1Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + + self.app.client_manager.identity = identity_fakes.FakeIdentityv2Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + + self.app.client_manager.image = FakeImagev1Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + + class FakeTransfer(object): """Fake one or more Transfer.""" @@ -345,61 +400,6 @@ def get_volumes(volumes=None, count=2): return mock.Mock(side_effect=volumes) -class FakeImagev1Client(object): - - def __init__(self, **kwargs): - self.images = mock.Mock() - - -class FakeVolumev1Client(object): - - def __init__(self, **kwargs): - self.volumes = mock.Mock() - self.volumes.resource_class = fakes.FakeResource(None, {}) - self.services = mock.Mock() - self.services.resource_class = fakes.FakeResource(None, {}) - self.extensions = mock.Mock() - self.extensions.resource_class = fakes.FakeResource(None, {}) - self.qos_specs = mock.Mock() - self.qos_specs.resource_class = fakes.FakeResource(None, {}) - self.volume_types = mock.Mock() - self.volume_types.resource_class = fakes.FakeResource(None, {}) - self.volume_encryption_types = mock.Mock() - self.volume_encryption_types.resource_class = ( - fakes.FakeResource(None, {})) - self.transfers = mock.Mock() - self.transfers.resource_class = fakes.FakeResource(None, {}) - self.volume_snapshots = mock.Mock() - self.volume_snapshots.resource_class = fakes.FakeResource(None, {}) - self.backups = mock.Mock() - self.backups.resource_class = fakes.FakeResource(None, {}) - self.restores = mock.Mock() - self.restores.resource_class = fakes.FakeResource(None, {}) - self.auth_token = kwargs['token'] - self.management_url = kwargs['endpoint'] - - -class TestVolumev1(utils.TestCommand): - - def setUp(self): - super(TestVolumev1, self).setUp() - - self.app.client_manager.volume = FakeVolumev1Client( - endpoint=fakes.AUTH_URL, - token=fakes.AUTH_TOKEN, - ) - - self.app.client_manager.identity = identity_fakes.FakeIdentityv2Client( - endpoint=fakes.AUTH_URL, - token=fakes.AUTH_TOKEN, - ) - - self.app.client_manager.image = FakeImagev1Client( - endpoint=fakes.AUTH_URL, - token=fakes.AUTH_TOKEN, - ) - - class FakeVolumeType(object): """Fake one or more type.""" diff --git a/openstackclient/tests/unit/volume/v2/fakes.py b/openstackclient/tests/unit/volume/v2/fakes.py index 6da69f8fcc..33588e045c 100644 --- a/openstackclient/tests/unit/volume/v2/fakes.py +++ b/openstackclient/tests/unit/volume/v2/fakes.py @@ -40,6 +40,73 @@ } +class FakeVolumeClient: + + def __init__(self, **kwargs): + self.auth_token = kwargs['token'] + self.management_url = kwargs['endpoint'] + self.api_version = api_versions.APIVersion('2.0') + + self.availability_zones = mock.Mock() + self.availability_zones.resource_class = fakes.FakeResource(None, {}) + self.backups = mock.Mock() + self.backups.resource_class = fakes.FakeResource(None, {}) + self.capabilities = mock.Mock() + self.capabilities.resource_class = fakes.FakeResource(None, {}) + self.cgsnapshots = mock.Mock() + self.cgsnapshots.resource_class = fakes.FakeResource(None, {}) + self.consistencygroups = mock.Mock() + self.consistencygroups.resource_class = fakes.FakeResource(None, {}) + self.extensions = mock.Mock() + self.extensions.resource_class = fakes.FakeResource(None, {}) + self.limits = mock.Mock() + self.limits.resource_class = fakes.FakeResource(None, {}) + self.pools = mock.Mock() + self.pools.resource_class = fakes.FakeResource(None, {}) + self.qos_specs = mock.Mock() + self.qos_specs.resource_class = fakes.FakeResource(None, {}) + self.quota_classes = mock.Mock() + self.quota_classes.resource_class = fakes.FakeResource(None, {}) + self.quotas = mock.Mock() + self.quotas.resource_class = fakes.FakeResource(None, {}) + self.restores = mock.Mock() + self.restores.resource_class = fakes.FakeResource(None, {}) + self.services = mock.Mock() + self.services.resource_class = fakes.FakeResource(None, {}) + self.transfers = mock.Mock() + self.transfers.resource_class = fakes.FakeResource(None, {}) + self.volume_encryption_types = mock.Mock() + self.volume_encryption_types.resource_class = ( + fakes.FakeResource(None, {})) + self.volume_snapshots = mock.Mock() + self.volume_snapshots.resource_class = fakes.FakeResource(None, {}) + self.volume_type_access = mock.Mock() + self.volume_type_access.resource_class = fakes.FakeResource(None, {}) + self.volume_types = mock.Mock() + self.volume_types.resource_class = fakes.FakeResource(None, {}) + self.volumes = mock.Mock() + self.volumes.resource_class = fakes.FakeResource(None, {}) + + +class TestVolume(utils.TestCommand): + + def setUp(self): + super().setUp() + + self.app.client_manager.volume = FakeVolumeClient( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN + ) + self.app.client_manager.identity = identity_fakes.FakeIdentityv3Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN + ) + self.app.client_manager.image = image_fakes.FakeImagev2Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN + ) + + class FakeTransfer(object): """Fake one or more Transfer.""" @@ -289,73 +356,6 @@ def create_one_pool(attrs=None): return pool -class FakeVolumeClient(object): - - def __init__(self, **kwargs): - self.auth_token = kwargs['token'] - self.management_url = kwargs['endpoint'] - self.api_version = api_versions.APIVersion('2.0') - - self.availability_zones = mock.Mock() - self.availability_zones.resource_class = fakes.FakeResource(None, {}) - self.backups = mock.Mock() - self.backups.resource_class = fakes.FakeResource(None, {}) - self.capabilities = mock.Mock() - self.capabilities.resource_class = fakes.FakeResource(None, {}) - self.cgsnapshots = mock.Mock() - self.cgsnapshots.resource_class = fakes.FakeResource(None, {}) - self.consistencygroups = mock.Mock() - self.consistencygroups.resource_class = fakes.FakeResource(None, {}) - self.extensions = mock.Mock() - self.extensions.resource_class = fakes.FakeResource(None, {}) - self.limits = mock.Mock() - self.limits.resource_class = fakes.FakeResource(None, {}) - self.pools = mock.Mock() - self.pools.resource_class = fakes.FakeResource(None, {}) - self.qos_specs = mock.Mock() - self.qos_specs.resource_class = fakes.FakeResource(None, {}) - self.quota_classes = mock.Mock() - self.quota_classes.resource_class = fakes.FakeResource(None, {}) - self.quotas = mock.Mock() - self.quotas.resource_class = fakes.FakeResource(None, {}) - self.restores = mock.Mock() - self.restores.resource_class = fakes.FakeResource(None, {}) - self.services = mock.Mock() - self.services.resource_class = fakes.FakeResource(None, {}) - self.transfers = mock.Mock() - self.transfers.resource_class = fakes.FakeResource(None, {}) - self.volume_encryption_types = mock.Mock() - self.volume_encryption_types.resource_class = ( - fakes.FakeResource(None, {})) - self.volume_snapshots = mock.Mock() - self.volume_snapshots.resource_class = fakes.FakeResource(None, {}) - self.volume_type_access = mock.Mock() - self.volume_type_access.resource_class = fakes.FakeResource(None, {}) - self.volume_types = mock.Mock() - self.volume_types.resource_class = fakes.FakeResource(None, {}) - self.volumes = mock.Mock() - self.volumes.resource_class = fakes.FakeResource(None, {}) - - -class TestVolume(utils.TestCommand): - - def setUp(self): - super(TestVolume, self).setUp() - - self.app.client_manager.volume = FakeVolumeClient( - endpoint=fakes.AUTH_URL, - token=fakes.AUTH_TOKEN - ) - self.app.client_manager.identity = identity_fakes.FakeIdentityv3Client( - endpoint=fakes.AUTH_URL, - token=fakes.AUTH_TOKEN - ) - self.app.client_manager.image = image_fakes.FakeImagev2Client( - endpoint=fakes.AUTH_URL, - token=fakes.AUTH_TOKEN - ) - - class FakeVolume(object): """Fake one or more volumes.""" diff --git a/openstackclient/tests/unit/volume/v3/fakes.py b/openstackclient/tests/unit/volume/v3/fakes.py index e27d7fcac5..caf7d2696f 100644 --- a/openstackclient/tests/unit/volume/v3/fakes.py +++ b/openstackclient/tests/unit/volume/v3/fakes.py @@ -23,7 +23,7 @@ from openstackclient.tests.unit.volume.v2 import fakes as volume_v2_fakes -class FakeVolumeClient(object): +class FakeVolumeClient: def __init__(self, **kwargs): self.auth_token = kwargs['token'] From d7f431be507f3f6904a503e5b0ce7fcc23caa326 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 30 Sep 2022 10:50:55 +0100 Subject: [PATCH 038/595] tests: Remove duplicate FakeImagev1Client I guess this was defined to avoid a circular import. There are easier ways to do this. Change-Id: Iab215a53691298413ea9d10def792008ab9edbce Signed-off-by: Stephen Finucane --- openstackclient/tests/unit/volume/v1/fakes.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/openstackclient/tests/unit/volume/v1/fakes.py b/openstackclient/tests/unit/volume/v1/fakes.py index c07af29914..2ec4019009 100644 --- a/openstackclient/tests/unit/volume/v1/fakes.py +++ b/openstackclient/tests/unit/volume/v1/fakes.py @@ -23,12 +23,6 @@ from openstackclient.tests.unit import utils -class FakeImagev1Client: - - def __init__(self, **kwargs): - self.images = mock.Mock() - - class FakeVolumev1Client: def __init__(self, **kwargs): @@ -72,7 +66,10 @@ def setUp(self): token=fakes.AUTH_TOKEN, ) - self.app.client_manager.image = FakeImagev1Client( + # avoid circular imports + from openstackclient.tests.unit.image.v1 import fakes as image_fakes + + self.app.client_manager.image = image_fakes.FakeImagev1Client( endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN, ) From 1aaaa6f1d14ebc2498513aabe788239c9f867639 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 29 Sep 2022 18:42:26 +0100 Subject: [PATCH 039/595] tests: Remove unnecessary nesting of volume resources Change-Id: I210ce7534d161e89115e5cb96e42ab7f27170aa1 Signed-off-by: Stephen Finucane --- .../unit/common/test_availability_zone.py | 7 +- .../tests/unit/common/test_extension.py | 8 +- .../tests/unit/common/test_limits.py | 4 +- .../tests/unit/common/test_project_purge.py | 10 +- .../tests/unit/common/test_quota.py | 23 +- .../tests/unit/compute/v2/test_server.py | 6 +- openstackclient/tests/unit/volume/v1/fakes.py | 1112 ++++----- .../tests/unit/volume/v1/test_qos_specs.py | 44 +- .../tests/unit/volume/v1/test_service.py | 14 +- .../unit/volume/v1/test_transfer_request.py | 47 +- .../tests/unit/volume/v1/test_type.py | 69 +- .../tests/unit/volume/v1/test_volume.py | 38 +- .../unit/volume/v1/test_volume_backup.py | 41 +- openstackclient/tests/unit/volume/v2/fakes.py | 2208 ++++++++--------- .../unit/volume/v2/test_backup_record.py | 20 +- .../unit/volume/v2/test_consistency_group.py | 70 +- .../v2/test_consistency_group_snapshot.py | 26 +- .../tests/unit/volume/v2/test_qos_specs.py | 26 +- .../tests/unit/volume/v2/test_service.py | 14 +- .../tests/unit/volume/v2/test_type.py | 94 +- .../tests/unit/volume/v2/test_volume.py | 56 +- .../unit/volume/v2/test_volume_backend.py | 8 +- .../unit/volume/v2/test_volume_backup.py | 38 +- .../tests/unit/volume/v2/test_volume_host.py | 14 +- .../unit/volume/v2/test_volume_snapshot.py | 18 +- .../volume/v2/test_volume_transfer_request.py | 47 +- openstackclient/tests/unit/volume/v3/fakes.py | 780 +++--- .../volume/v3/test_block_storage_cluster.py | 6 +- .../v3/test_block_storage_resource_filter.py | 6 +- .../unit/volume/v3/test_volume_attachment.py | 20 +- .../tests/unit/volume/v3/test_volume_group.py | 19 +- .../volume/v3/test_volume_group_snapshot.py | 9 +- .../unit/volume/v3/test_volume_group_type.py | 29 +- .../unit/volume/v3/test_volume_message.py | 14 +- 34 files changed, 2378 insertions(+), 2567 deletions(-) diff --git a/openstackclient/tests/unit/common/test_availability_zone.py b/openstackclient/tests/unit/common/test_availability_zone.py index e5348ec3e9..096038cafe 100644 --- a/openstackclient/tests/unit/common/test_availability_zone.py +++ b/openstackclient/tests/unit/common/test_availability_zone.py @@ -78,7 +78,7 @@ def _build_network_az_datalist(network_az, long_datalist=False): class TestAvailabilityZone(utils.TestCommand): def setUp(self): - super(TestAvailabilityZone, self).setUp() + super().setUp() compute_client = compute_fakes.FakeComputev2Client( endpoint=fakes.AUTH_URL, @@ -113,8 +113,7 @@ class TestAvailabilityZoneList(TestAvailabilityZone): compute_azs = \ compute_fakes.FakeAvailabilityZone.create_availability_zones() - volume_azs = \ - volume_fakes.FakeAvailabilityZone.create_availability_zones(count=1) + volume_azs = volume_fakes.create_availability_zones(count=1) network_azs = network_fakes.create_availability_zones() short_columnslist = ('Zone Name', 'Zone Status') @@ -128,7 +127,7 @@ class TestAvailabilityZoneList(TestAvailabilityZone): ) def setUp(self): - super(TestAvailabilityZoneList, self).setUp() + super().setUp() self.compute_azs_mock.list.return_value = self.compute_azs self.volume_azs_mock.list.return_value = self.volume_azs diff --git a/openstackclient/tests/unit/common/test_extension.py b/openstackclient/tests/unit/common/test_extension.py index 5093cbbb09..bd90b32d1b 100644 --- a/openstackclient/tests/unit/common/test_extension.py +++ b/openstackclient/tests/unit/common/test_extension.py @@ -26,7 +26,7 @@ class TestExtension(utils.TestCommand): def setUp(self): - super(TestExtension, self).setUp() + super().setUp() identity_client = identity_fakes.FakeIdentityv2Client( endpoint=fakes.AUTH_URL, @@ -66,13 +66,13 @@ class TestExtensionList(TestExtension): long_columns = ('Name', 'Alias', 'Description', 'Namespace', 'Updated', 'Links') - volume_extension = volume_fakes.FakeExtension.create_one_extension() + volume_extension = volume_fakes.create_one_extension() identity_extension = identity_fakes.FakeExtension.create_one_extension() compute_extension = compute_fakes.FakeExtension.create_one_extension() network_extension = network_fakes.FakeExtension.create_one_extension() def setUp(self): - super(TestExtensionList, self).setUp() + super().setUp() self.identity_extensions_mock.list.return_value = [ self.identity_extension] @@ -310,7 +310,7 @@ class TestExtensionShow(TestExtension): ) def setUp(self): - super(TestExtensionShow, self).setUp() + super().setUp() self.cmd = extension.ShowExtension(self.app, None) diff --git a/openstackclient/tests/unit/common/test_limits.py b/openstackclient/tests/unit/common/test_limits.py index d73db2cb17..e3cdcf45a2 100644 --- a/openstackclient/tests/unit/common/test_limits.py +++ b/openstackclient/tests/unit/common/test_limits.py @@ -33,7 +33,7 @@ class TestComputeLimits(compute_fakes.TestComputev2): ] def setUp(self): - super(TestComputeLimits, self).setUp() + super().setUp() self.app.client_manager.volume_endpoint_enabled = False self.compute = self.app.client_manager.compute @@ -87,7 +87,7 @@ class TestVolumeLimits(volume_fakes.TestVolume): ] def setUp(self): - super(TestVolumeLimits, self).setUp() + super().setUp() self.app.client_manager.compute_endpoint_enabled = False self.volume = self.app.client_manager.volume diff --git a/openstackclient/tests/unit/common/test_project_purge.py b/openstackclient/tests/unit/common/test_project_purge.py index 5199093ce3..26333d7067 100644 --- a/openstackclient/tests/unit/common/test_project_purge.py +++ b/openstackclient/tests/unit/common/test_project_purge.py @@ -26,7 +26,7 @@ class TestProjectPurgeInit(tests_utils.TestCommand): def setUp(self): - super(TestProjectPurgeInit, self).setUp() + super().setUp() compute_client = compute_fakes.FakeComputev2Client( endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN, @@ -71,12 +71,12 @@ class TestProjectPurge(TestProjectPurgeInit): project = identity_fakes.FakeProject.create_one_project() server = compute_fakes.FakeServer.create_one_server() image = image_fakes.create_one_image() - volume = volume_fakes.FakeVolume.create_one_volume() - backup = volume_fakes.FakeBackup.create_one_backup() - snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() + volume = volume_fakes.create_one_volume() + backup = volume_fakes.create_one_backup() + snapshot = volume_fakes.create_one_snapshot() def setUp(self): - super(TestProjectPurge, self).setUp() + super().setUp() self.projects_mock.get.return_value = self.project self.projects_mock.delete.return_value = None self.images_mock.list.return_value = [self.image] diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index 0900200e96..682799b39e 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -176,12 +176,12 @@ def setUp(self): ) self.volume_quotas = [ - volume_fakes.FakeQuota.create_one_vol_quota(), - volume_fakes.FakeQuota.create_one_vol_quota(), + volume_fakes.create_one_vol_quota(), + volume_fakes.create_one_vol_quota(), ] self.volume_default_quotas = [ - volume_fakes.FakeQuota.create_one_default_vol_quota(), - volume_fakes.FakeQuota.create_one_default_vol_quota(), + volume_fakes.create_one_default_vol_quota(), + volume_fakes.create_one_default_vol_quota(), ] self.volume = self.app.client_manager.volume self.volume.quotas.defaults = mock.Mock( @@ -280,8 +280,7 @@ def test_quota_list_details_network(self): sorted(detailed_reference_data), sorted(ret_quotas)) def test_quota_list_details_volume(self): - detailed_quota = ( - volume_fakes.FakeQuota.create_one_detailed_quota()) + detailed_quota = volume_fakes.create_one_detailed_quota() detailed_column_header = ( 'Resource', @@ -567,7 +566,7 @@ def test_quota_list_volume_default(self): self.volume.quotas.get = mock.Mock( side_effect=[ self.volume_quotas[0], - volume_fakes.FakeQuota.create_one_default_vol_quota(), + volume_fakes.create_one_default_vol_quota(), ], ) @@ -591,7 +590,7 @@ def test_quota_list_volume_no_project(self): self.volume.quotas.get = mock.Mock( side_effect=[ self.volume_quotas[0], - volume_fakes.FakeQuota.create_one_default_vol_quota(), + volume_fakes.create_one_default_vol_quota(), ], ) @@ -1046,10 +1045,9 @@ def setUp(self): loaded=True, ) - self.volume_quota = volume_fakes.FakeQuota.create_one_vol_quota() + self.volume_quota = volume_fakes.create_one_vol_quota() self.volume_quotas_mock.get.return_value = self.volume_quota - self.volume_default_quota = \ - volume_fakes.FakeQuota.create_one_default_vol_quota() + self.volume_default_quota = volume_fakes.create_one_default_vol_quota() self.volume_quotas_mock.defaults.return_value = \ self.volume_default_quota self.volume_quotas_class_mock.get.return_value = FakeQuotaResource( @@ -1217,8 +1215,7 @@ def test_quota_show__with_usage(self): self.compute_quota = \ compute_fakes.FakeQuota.create_one_comp_detailed_quota() self.compute_quotas_mock.get.return_value = self.compute_quota - self.volume_quota = \ - volume_fakes.FakeQuota.create_one_detailed_quota() + self.volume_quota = volume_fakes.create_one_detailed_quota() self.volume_quotas_mock.get.return_value = self.volume_quota self.network.get_quota.return_value = \ network_fakes.FakeQuota.create_one_net_detailed_quota() diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index d393acdaf1..3442da6b9b 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -153,7 +153,7 @@ def setup_sdk_servers_mock(self, count): return servers def setup_sdk_volumes_mock(self, count): - volumes = volume_fakes.FakeVolume.create_sdk_volumes(count=count) + volumes = volume_fakes.create_sdk_volumes(count=count) # This is the return value for volume_client.find_volume() self.sdk_volume_client.find_volume.side_effect = volumes @@ -1444,10 +1444,10 @@ def setUp(self): self.flavor = compute_fakes.FakeFlavor.create_one_flavor() self.flavors_mock.get.return_value = self.flavor - self.volume = volume_fakes.FakeVolume.create_one_volume() + self.volume = volume_fakes.create_one_volume() self.volumes_mock.get.return_value = self.volume - self.snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() + self.snapshot = volume_fakes.create_one_snapshot() self.snapshots_mock.get.return_value = self.snapshot # Get the command object to test diff --git a/openstackclient/tests/unit/volume/v1/fakes.py b/openstackclient/tests/unit/volume/v1/fakes.py index 2ec4019009..76b208b2f1 100644 --- a/openstackclient/tests/unit/volume/v1/fakes.py +++ b/openstackclient/tests/unit/volume/v1/fakes.py @@ -24,7 +24,6 @@ class FakeVolumev1Client: - def __init__(self, **kwargs): self.volumes = mock.Mock() self.volumes.resource_class = fakes.FakeResource(None, {}) @@ -37,8 +36,9 @@ def __init__(self, **kwargs): self.volume_types = mock.Mock() self.volume_types.resource_class = fakes.FakeResource(None, {}) self.volume_encryption_types = mock.Mock() - self.volume_encryption_types.resource_class = ( - fakes.FakeResource(None, {})) + self.volume_encryption_types.resource_class = fakes.FakeResource( + None, {} + ) self.transfers = mock.Mock() self.transfers.resource_class = fakes.FakeResource(None, {}) self.volume_snapshots = mock.Mock() @@ -52,7 +52,6 @@ def __init__(self, **kwargs): class TestVolumev1(utils.TestCommand): - def setUp(self): super().setUp() @@ -75,577 +74,538 @@ def setUp(self): ) -class FakeTransfer(object): - """Fake one or more Transfer.""" - - @staticmethod - def create_one_transfer(attrs=None): - """Create a fake transfer. - - :param Dictionary attrs: - A dictionary with all attributes of Transfer Request - :return: - A FakeResource object with volume_id, name, id. - """ - # Set default attribute - transfer_info = { - 'volume_id': 'volume-id-' + uuid.uuid4().hex, - 'name': 'fake_transfer_name', - 'id': 'id-' + uuid.uuid4().hex, - 'links': 'links-' + uuid.uuid4().hex, - } - - # Overwrite default attributes if there are some attributes set - attrs = attrs or {} - - transfer_info.update(attrs) - - transfer = fakes.FakeResource( - None, - transfer_info, - loaded=True) - - return transfer - - @staticmethod - def create_transfers(attrs=None, count=2): - """Create multiple fake transfers. - - :param Dictionary attrs: - A dictionary with all attributes of transfer - :param Integer count: - The number of transfers to be faked - :return: - A list of FakeResource objects - """ - transfers = [] - for n in range(0, count): - transfers.append(FakeTransfer.create_one_transfer(attrs)) - - return transfers - - @staticmethod - def get_transfers(transfers=None, count=2): - """Get an iterable MagicMock object with a list of faked transfers. - - If transfers list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param List transfers: - A list of FakeResource objects faking transfers - :param Integer count: - The number of transfers to be faked - :return - An iterable Mock object with side_effect set to a list of faked - transfers - """ - if transfers is None: - transfers = FakeTransfer.create_transfers(count) - - return mock.Mock(side_effect=transfers) - - -class FakeService(object): - """Fake one or more Services.""" - - @staticmethod - def create_one_service(attrs=None): - """Create a fake service. - - :param Dictionary attrs: - A dictionary with all attributes of service - :return: - A FakeResource object with host, status, etc. - """ - # Set default attribute - service_info = { - 'host': 'host_test', - 'binary': 'cinder_test', - 'status': 'enabled', - 'disabled_reason': 'LongHoliday-GoldenWeek', - 'zone': 'fake_zone', - 'updated_at': 'fake_date', - 'state': 'fake_state', - } - - # Overwrite default attributes if there are some attributes set - attrs = attrs or {} - - service_info.update(attrs) - - service = fakes.FakeResource( - None, - service_info, - loaded=True) - - return service - - @staticmethod - def create_services(attrs=None, count=2): - """Create multiple fake services. - - :param Dictionary attrs: - A dictionary with all attributes of service - :param Integer count: - The number of services to be faked - :return: - A list of FakeResource objects - """ - services = [] - for n in range(0, count): - services.append(FakeService.create_one_service(attrs)) - - return services - - @staticmethod - def get_services(services=None, count=2): - """Get an iterable MagicMock object with a list of faked services. - - If services list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param List services: - A list of FakeResource objects faking services - :param Integer count: - The number of services to be faked - :return - An iterable Mock object with side_effect set to a list of faked - services - """ - if services is None: - services = FakeService.create_services(count) - - return mock.Mock(side_effect=services) - - -class FakeQos(object): - """Fake one or more Qos specification.""" - - @staticmethod - def create_one_qos(attrs=None): - """Create a fake Qos specification. - - :param Dictionary attrs: - A dictionary with all attributes - :return: - A FakeResource object with id, name, consumer, etc. - """ - attrs = attrs or {} - - # Set default attributes. - qos_info = { - "id": 'qos-id-' + uuid.uuid4().hex, - "name": 'qos-name-' + uuid.uuid4().hex, - "consumer": 'front-end', - "specs": {"foo": "bar", "iops": "9001"}, - } - - # Overwrite default attributes. - qos_info.update(attrs) - - qos = fakes.FakeResource( - info=copy.deepcopy(qos_info), - loaded=True) - return qos - - @staticmethod - def create_one_qos_association(attrs=None): - """Create a fake Qos specification association. - - :param Dictionary attrs: - A dictionary with all attributes - :return: - A FakeResource object with id, name, association_type, etc. - """ - attrs = attrs or {} - - # Set default attributes. - qos_association_info = { - "id": 'type-id-' + uuid.uuid4().hex, - "name": 'type-name-' + uuid.uuid4().hex, - "association_type": 'volume_type', - } - - # Overwrite default attributes. - qos_association_info.update(attrs) - - qos_association = fakes.FakeResource( - info=copy.deepcopy(qos_association_info), - loaded=True) - return qos_association - - @staticmethod - def create_qoses(attrs=None, count=2): - """Create multiple fake Qos specifications. - - :param Dictionary attrs: - A dictionary with all attributes - :param int count: - The number of Qos specifications to fake - :return: - A list of FakeResource objects faking the Qos specifications - """ - qoses = [] - for i in range(0, count): - qos = FakeQos.create_one_qos(attrs) - qoses.append(qos) - - return qoses - - @staticmethod - def get_qoses(qoses=None, count=2): - """Get an iterable MagicMock object with a list of faked qoses. - - If qoses list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param List volumes: - A list of FakeResource objects faking qoses - :param Integer count: - The number of qoses to be faked - :return - An iterable Mock object with side_effect set to a list of faked - qoses - """ - if qoses is None: - qoses = FakeQos.create_qoses(count) - - return mock.Mock(side_effect=qoses) - - -class FakeVolume(object): - """Fake one or more volumes.""" - - @staticmethod - def create_one_volume(attrs=None): - """Create a fake volume. - - :param Dictionary attrs: - A dictionary with all attributes of volume - :return: - A FakeResource object with id, name, status, etc. - """ - attrs = attrs or {} - - # Set default attribute - volume_info = { - 'id': 'volume-id' + uuid.uuid4().hex, - 'display_name': 'volume-name' + uuid.uuid4().hex, - 'display_description': 'description' + uuid.uuid4().hex, - 'status': 'available', - 'size': 10, - 'volume_type': - random.choice(['fake_lvmdriver-1', 'fake_lvmdriver-2']), - 'bootable': 'true', - 'metadata': { - 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, - 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, - 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex}, - 'snapshot_id': 'snapshot-id-' + uuid.uuid4().hex, - 'availability_zone': 'zone' + uuid.uuid4().hex, - 'attachments': [{ +def create_one_transfer(attrs=None): + """Create a fake transfer. + + :param Dictionary attrs: + A dictionary with all attributes of Transfer Request + :return: + A FakeResource object with volume_id, name, id. + """ + # Set default attribute + transfer_info = { + 'volume_id': 'volume-id-' + uuid.uuid4().hex, + 'name': 'fake_transfer_name', + 'id': 'id-' + uuid.uuid4().hex, + 'links': 'links-' + uuid.uuid4().hex, + } + + # Overwrite default attributes if there are some attributes set + attrs = attrs or {} + + transfer_info.update(attrs) + + transfer = fakes.FakeResource(None, transfer_info, loaded=True) + + return transfer + + +def create_transfers(attrs=None, count=2): + """Create multiple fake transfers. + + :param Dictionary attrs: + A dictionary with all attributes of transfer + :param Integer count: + The number of transfers to be faked + :return: + A list of FakeResource objects + """ + transfers = [] + for n in range(0, count): + transfers.append(create_one_transfer(attrs)) + + return transfers + + +def get_transfers(transfers=None, count=2): + """Get an iterable MagicMock object with a list of faked transfers. + + If transfers list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List transfers: + A list of FakeResource objects faking transfers + :param Integer count: + The number of transfers to be faked + :return + An iterable Mock object with side_effect set to a list of faked + transfers + """ + if transfers is None: + transfers = create_transfers(count) + + return mock.Mock(side_effect=transfers) + + +def create_one_service(attrs=None): + """Create a fake service. + + :param Dictionary attrs: + A dictionary with all attributes of service + :return: + A FakeResource object with host, status, etc. + """ + # Set default attribute + service_info = { + 'host': 'host_test', + 'binary': 'cinder_test', + 'status': 'enabled', + 'disabled_reason': 'LongHoliday-GoldenWeek', + 'zone': 'fake_zone', + 'updated_at': 'fake_date', + 'state': 'fake_state', + } + + # Overwrite default attributes if there are some attributes set + attrs = attrs or {} + + service_info.update(attrs) + + service = fakes.FakeResource(None, service_info, loaded=True) + + return service + + +def create_services(attrs=None, count=2): + """Create multiple fake services. + + :param Dictionary attrs: + A dictionary with all attributes of service + :param Integer count: + The number of services to be faked + :return: + A list of FakeResource objects + """ + services = [] + for n in range(0, count): + services.append(create_one_service(attrs)) + + return services + + +def get_services(services=None, count=2): + """Get an iterable MagicMock object with a list of faked services. + + If services list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List services: + A list of FakeResource objects faking services + :param Integer count: + The number of services to be faked + :return + An iterable Mock object with side_effect set to a list of faked + services + """ + if services is None: + services = create_services(count) + + return mock.Mock(side_effect=services) + + +def create_one_qos(attrs=None): + """Create a fake Qos specification. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object with id, name, consumer, etc. + """ + attrs = attrs or {} + + # Set default attributes. + qos_info = { + "id": 'qos-id-' + uuid.uuid4().hex, + "name": 'qos-name-' + uuid.uuid4().hex, + "consumer": 'front-end', + "specs": {"foo": "bar", "iops": "9001"}, + } + + # Overwrite default attributes. + qos_info.update(attrs) + + qos = fakes.FakeResource(info=copy.deepcopy(qos_info), loaded=True) + return qos + + +def create_one_qos_association(attrs=None): + """Create a fake Qos specification association. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object with id, name, association_type, etc. + """ + attrs = attrs or {} + + # Set default attributes. + qos_association_info = { + "id": 'type-id-' + uuid.uuid4().hex, + "name": 'type-name-' + uuid.uuid4().hex, + "association_type": 'volume_type', + } + + # Overwrite default attributes. + qos_association_info.update(attrs) + + qos_association = fakes.FakeResource( + info=copy.deepcopy(qos_association_info), loaded=True + ) + return qos_association + + +def create_qoses(attrs=None, count=2): + """Create multiple fake Qos specifications. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of Qos specifications to fake + :return: + A list of FakeResource objects faking the Qos specifications + """ + qoses = [] + for i in range(0, count): + qos = create_one_qos(attrs) + qoses.append(qos) + + return qoses + + +def get_qoses(qoses=None, count=2): + """Get an iterable MagicMock object with a list of faked qoses. + + If qoses list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List volumes: + A list of FakeResource objects faking qoses + :param Integer count: + The number of qoses to be faked + :return + An iterable Mock object with side_effect set to a list of faked + qoses + """ + if qoses is None: + qoses = create_qoses(count) + + return mock.Mock(side_effect=qoses) + + +def create_one_volume(attrs=None): + """Create a fake volume. + + :param Dictionary attrs: + A dictionary with all attributes of volume + :return: + A FakeResource object with id, name, status, etc. + """ + attrs = attrs or {} + + # Set default attribute + volume_info = { + 'id': 'volume-id' + uuid.uuid4().hex, + 'display_name': 'volume-name' + uuid.uuid4().hex, + 'display_description': 'description' + uuid.uuid4().hex, + 'status': 'available', + 'size': 10, + 'volume_type': random.choice(['fake_lvmdriver-1', 'fake_lvmdriver-2']), + 'bootable': 'true', + 'metadata': { + 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, + 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, + 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, + }, + 'snapshot_id': 'snapshot-id-' + uuid.uuid4().hex, + 'availability_zone': 'zone' + uuid.uuid4().hex, + 'attachments': [ + { 'device': '/dev/' + uuid.uuid4().hex, 'server_id': uuid.uuid4().hex, - }, ], - 'created_at': 'time-' + uuid.uuid4().hex, - } - - # Overwrite default attributes if there are some attributes set - volume_info.update(attrs) - - volume = fakes.FakeResource( - None, - volume_info, - loaded=True) - return volume - - @staticmethod - def create_volumes(attrs=None, count=2): - """Create multiple fake volumes. - - :param Dictionary attrs: - A dictionary with all attributes of volume - :param Integer count: - The number of volumes to be faked - :return: - A list of FakeResource objects - """ - volumes = [] - for n in range(0, count): - volumes.append(FakeVolume.create_one_volume(attrs)) - - return volumes - - @staticmethod - def get_volumes(volumes=None, count=2): - """Get an iterable MagicMock object with a list of faked volumes. - - If volumes list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param List volumes: - A list of FakeResource objects faking volumes - :param Integer count: - The number of volumes to be faked - :return - An iterable Mock object with side_effect set to a list of faked - volumes - """ - if volumes is None: - volumes = FakeVolume.create_volumes(count) - - return mock.Mock(side_effect=volumes) - - -class FakeVolumeType(object): - """Fake one or more type.""" - - @staticmethod - def create_one_volume_type(attrs=None, methods=None): - """Create a fake volume type. - - :param Dictionary attrs: - A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods - :return: - A FakeResource object with id, name, description, etc. - """ - attrs = attrs or {} - methods = methods or {} - - # Set default attributes. - volume_type_info = { - "id": 'type-id-' + uuid.uuid4().hex, - "name": 'type-name-' + uuid.uuid4().hex, - "description": 'type-description-' + uuid.uuid4().hex, - "extra_specs": {"foo": "bar"}, - "is_public": True, - } - - # Overwrite default attributes. - volume_type_info.update(attrs) - - volume_type = fakes.FakeResource( - info=copy.deepcopy(volume_type_info), - methods=methods, - loaded=True) - return volume_type - - @staticmethod - def create_volume_types(attrs=None, count=2): - """Create multiple fake types. - - :param Dictionary attrs: - A dictionary with all attributes - :param int count: - The number of types to fake - :return: - A list of FakeResource objects faking the types - """ - volume_types = [] - for i in range(0, count): - volume_type = FakeVolumeType.create_one_volume_type(attrs) - volume_types.append(volume_type) - - return volume_types - - @staticmethod - def get_volume_types(volume_types=None, count=2): - """Get an iterable MagicMock object with a list of faked types. - - If types list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param List volume_types: - A list of FakeResource objects faking types - :param Integer count: - The number of types to be faked - :return - An iterable Mock object with side_effect set to a list of faked - types - """ - if volume_types is None: - volume_types = FakeVolumeType.create_volume_types(count) - - return mock.Mock(side_effect=volume_types) - - @staticmethod - def create_one_encryption_volume_type(attrs=None): - """Create a fake encryption volume type. - - :param Dictionary attrs: - A dictionary with all attributes - :return: - A FakeResource object with volume_type_id etc. - """ - attrs = attrs or {} - - # Set default attributes. - encryption_info = { - "volume_type_id": 'type-id-' + uuid.uuid4().hex, - 'provider': 'LuksEncryptor', - 'cipher': None, - 'key_size': None, - 'control_location': 'front-end', - } - - # Overwrite default attributes. - encryption_info.update(attrs) - - encryption_type = fakes.FakeResource( - info=copy.deepcopy(encryption_info), - loaded=True) - return encryption_type - - -class FakeSnapshot(object): - """Fake one or more snapshot.""" - - @staticmethod - def create_one_snapshot(attrs=None): - """Create a fake snapshot. - - :param Dictionary attrs: - A dictionary with all attributes - :return: - A FakeResource object with id, name, description, etc. - """ - attrs = attrs or {} - - # Set default attributes. - snapshot_info = { - "id": 'snapshot-id-' + uuid.uuid4().hex, - "display_name": 'snapshot-name-' + uuid.uuid4().hex, - "display_description": 'snapshot-description-' + uuid.uuid4().hex, - "size": 10, - "status": "available", - "metadata": {"foo": "bar"}, - "created_at": "2015-06-03T18:49:19.000000", - "volume_id": 'vloume-id-' + uuid.uuid4().hex, - } - - # Overwrite default attributes. - snapshot_info.update(attrs) - - snapshot_method = {'update': None} - - snapshot = fakes.FakeResource( - info=copy.deepcopy(snapshot_info), - methods=copy.deepcopy(snapshot_method), - loaded=True) - return snapshot - - @staticmethod - def create_snapshots(attrs=None, count=2): - """Create multiple fake snapshots. - - :param Dictionary attrs: - A dictionary with all attributes - :param int count: - The number of snapshots to fake - :return: - A list of FakeResource objects faking the snapshots - """ - snapshots = [] - for i in range(0, count): - snapshot = FakeSnapshot.create_one_snapshot(attrs) - snapshots.append(snapshot) - - return snapshots - - @staticmethod - def get_snapshots(snapshots=None, count=2): - """Get an iterable MagicMock object with a list of faked snapshots. - - If snapshots list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param List volumes: - A list of FakeResource objects faking snapshots - :param Integer count: - The number of snapshots to be faked - :return - An iterable Mock object with side_effect set to a list of faked - snapshots - """ - if snapshots is None: - snapshots = FakeSnapshot.create_snapshots(count) - - return mock.Mock(side_effect=snapshots) - - -class FakeBackup(object): - """Fake one or more backup.""" - - @staticmethod - def create_one_backup(attrs=None): - """Create a fake backup. - - :param Dictionary attrs: - A dictionary with all attributes - :return: - A FakeResource object with id, name, volume_id, etc. - """ - attrs = attrs or {} - - # Set default attributes. - backup_info = { - "id": 'backup-id-' + uuid.uuid4().hex, - "name": 'backup-name-' + uuid.uuid4().hex, - "volume_id": 'volume-id-' + uuid.uuid4().hex, - "snapshot_id": 'snapshot-id' + uuid.uuid4().hex, - "description": 'description-' + uuid.uuid4().hex, - "object_count": None, - "container": 'container-' + uuid.uuid4().hex, - "size": random.randint(1, 20), - "status": "error", - "availability_zone": 'zone' + uuid.uuid4().hex, - "links": 'links-' + uuid.uuid4().hex, - } - - # Overwrite default attributes. - backup_info.update(attrs) - - backup = fakes.FakeResource( - info=copy.deepcopy(backup_info), - loaded=True) - return backup - - @staticmethod - def create_backups(attrs=None, count=2): - """Create multiple fake backups. - - :param Dictionary attrs: - A dictionary with all attributes - :param int count: - The number of backups to fake - :return: - A list of FakeResource objects faking the backups - """ - backups = [] - for i in range(0, count): - backup = FakeBackup.create_one_backup(attrs) - backups.append(backup) - - return backups - - @staticmethod - def get_backups(backups=None, count=2): - """Get an iterable MagicMock object with a list of faked backups. - - If backups list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param List volumes: - A list of FakeResource objects faking backups - :param Integer count: - The number of backups to be faked - :return - An iterable Mock object with side_effect set to a list of faked - backups - """ - if backups is None: - backups = FakeBackup.create_backups(count) - - return mock.Mock(side_effect=backups) + }, + ], + 'created_at': 'time-' + uuid.uuid4().hex, + } + + # Overwrite default attributes if there are some attributes set + volume_info.update(attrs) + + volume = fakes.FakeResource(None, volume_info, loaded=True) + return volume + + +def create_volumes(attrs=None, count=2): + """Create multiple fake volumes. + + :param Dictionary attrs: + A dictionary with all attributes of volume + :param Integer count: + The number of volumes to be faked + :return: + A list of FakeResource objects + """ + volumes = [] + for n in range(0, count): + volumes.append(create_one_volume(attrs)) + + return volumes + + +def get_volumes(volumes=None, count=2): + """Get an iterable MagicMock object with a list of faked volumes. + + If volumes list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List volumes: + A list of FakeResource objects faking volumes + :param Integer count: + The number of volumes to be faked + :return + An iterable Mock object with side_effect set to a list of faked + volumes + """ + if volumes is None: + volumes = create_volumes(count) + + return mock.Mock(side_effect=volumes) + + +def create_one_volume_type(attrs=None, methods=None): + """Create a fake volume type. + + :param Dictionary attrs: + A dictionary with all attributes + :param Dictionary methods: + A dictionary with all methods + :return: + A FakeResource object with id, name, description, etc. + """ + attrs = attrs or {} + methods = methods or {} + + # Set default attributes. + volume_type_info = { + "id": 'type-id-' + uuid.uuid4().hex, + "name": 'type-name-' + uuid.uuid4().hex, + "description": 'type-description-' + uuid.uuid4().hex, + "extra_specs": {"foo": "bar"}, + "is_public": True, + } + + # Overwrite default attributes. + volume_type_info.update(attrs) + + volume_type = fakes.FakeResource( + info=copy.deepcopy(volume_type_info), methods=methods, loaded=True + ) + return volume_type + + +def create_volume_types(attrs=None, count=2): + """Create multiple fake types. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of types to fake + :return: + A list of FakeResource objects faking the types + """ + volume_types = [] + for i in range(0, count): + volume_type = create_one_volume_type(attrs) + volume_types.append(volume_type) + + return volume_types + + +def get_volume_types(volume_types=None, count=2): + """Get an iterable MagicMock object with a list of faked types. + + If types list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List volume_types: + A list of FakeResource objects faking types + :param Integer count: + The number of types to be faked + :return + An iterable Mock object with side_effect set to a list of faked + types + """ + if volume_types is None: + volume_types = create_volume_types(count) + + return mock.Mock(side_effect=volume_types) + + +def create_one_encryption_volume_type(attrs=None): + """Create a fake encryption volume type. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object with volume_type_id etc. + """ + attrs = attrs or {} + + # Set default attributes. + encryption_info = { + "volume_type_id": 'type-id-' + uuid.uuid4().hex, + 'provider': 'LuksEncryptor', + 'cipher': None, + 'key_size': None, + 'control_location': 'front-end', + } + + # Overwrite default attributes. + encryption_info.update(attrs) + + encryption_type = fakes.FakeResource( + info=copy.deepcopy(encryption_info), loaded=True + ) + return encryption_type + + +def create_one_snapshot(attrs=None): + """Create a fake snapshot. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object with id, name, description, etc. + """ + attrs = attrs or {} + + # Set default attributes. + snapshot_info = { + "id": 'snapshot-id-' + uuid.uuid4().hex, + "display_name": 'snapshot-name-' + uuid.uuid4().hex, + "display_description": 'snapshot-description-' + uuid.uuid4().hex, + "size": 10, + "status": "available", + "metadata": {"foo": "bar"}, + "created_at": "2015-06-03T18:49:19.000000", + "volume_id": 'vloume-id-' + uuid.uuid4().hex, + } + + # Overwrite default attributes. + snapshot_info.update(attrs) + + snapshot_method = {'update': None} + + snapshot = fakes.FakeResource( + info=copy.deepcopy(snapshot_info), + methods=copy.deepcopy(snapshot_method), + loaded=True, + ) + return snapshot + + +def create_snapshots(attrs=None, count=2): + """Create multiple fake snapshots. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of snapshots to fake + :return: + A list of FakeResource objects faking the snapshots + """ + snapshots = [] + for i in range(0, count): + snapshot = create_one_snapshot(attrs) + snapshots.append(snapshot) + + return snapshots + + +def get_snapshots(snapshots=None, count=2): + """Get an iterable MagicMock object with a list of faked snapshots. + + If snapshots list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List volumes: + A list of FakeResource objects faking snapshots + :param Integer count: + The number of snapshots to be faked + :return + An iterable Mock object with side_effect set to a list of faked + snapshots + """ + if snapshots is None: + snapshots = create_snapshots(count) + + return mock.Mock(side_effect=snapshots) + + +def create_one_backup(attrs=None): + """Create a fake backup. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object with id, name, volume_id, etc. + """ + attrs = attrs or {} + + # Set default attributes. + backup_info = { + "id": 'backup-id-' + uuid.uuid4().hex, + "name": 'backup-name-' + uuid.uuid4().hex, + "volume_id": 'volume-id-' + uuid.uuid4().hex, + "snapshot_id": 'snapshot-id' + uuid.uuid4().hex, + "description": 'description-' + uuid.uuid4().hex, + "object_count": None, + "container": 'container-' + uuid.uuid4().hex, + "size": random.randint(1, 20), + "status": "error", + "availability_zone": 'zone' + uuid.uuid4().hex, + "links": 'links-' + uuid.uuid4().hex, + } + + # Overwrite default attributes. + backup_info.update(attrs) + + backup = fakes.FakeResource(info=copy.deepcopy(backup_info), loaded=True) + return backup + + +def create_backups(attrs=None, count=2): + """Create multiple fake backups. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of backups to fake + :return: + A list of FakeResource objects faking the backups + """ + backups = [] + for i in range(0, count): + backup = create_one_backup(attrs) + backups.append(backup) + + return backups + + +def get_backups(backups=None, count=2): + """Get an iterable MagicMock object with a list of faked backups. + + If backups list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List volumes: + A list of FakeResource objects faking backups + :param Integer count: + The number of backups to be faked + :return + An iterable Mock object with side_effect set to a list of faked + backups + """ + if backups is None: + backups = create_backups(count) + + return mock.Mock(side_effect=backups) diff --git a/openstackclient/tests/unit/volume/v1/test_qos_specs.py b/openstackclient/tests/unit/volume/v1/test_qos_specs.py index d66a8558b2..f5b35143a6 100644 --- a/openstackclient/tests/unit/volume/v1/test_qos_specs.py +++ b/openstackclient/tests/unit/volume/v1/test_qos_specs.py @@ -28,7 +28,7 @@ class TestQos(volume_fakes.TestVolumev1): def setUp(self): - super(TestQos, self).setUp() + super().setUp() self.qos_mock = self.app.client_manager.volume.qos_specs self.qos_mock.reset_mock() @@ -39,11 +39,11 @@ def setUp(self): class TestQosAssociate(TestQos): - volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() - qos_spec = volume_fakes.FakeQos.create_one_qos() + volume_type = volume_fakes.create_one_volume_type() + qos_spec = volume_fakes.create_one_qos() def setUp(self): - super(TestQosAssociate, self).setUp() + super().setUp() self.qos_mock.get.return_value = self.qos_spec self.types_mock.get.return_value = self.volume_type @@ -80,8 +80,8 @@ class TestQosCreate(TestQos): ) def setUp(self): - super(TestQosCreate, self).setUp() - self.new_qos_spec = volume_fakes.FakeQos.create_one_qos() + super().setUp() + self.new_qos_spec = volume_fakes.create_one_qos() self.datalist = ( self.new_qos_spec.consumer, self.new_qos_spec.id, @@ -160,13 +160,13 @@ def test_qos_create_with_properties(self): class TestQosDelete(TestQos): - qos_specs = volume_fakes.FakeQos.create_qoses(count=2) + qos_specs = volume_fakes.create_qoses(count=2) def setUp(self): - super(TestQosDelete, self).setUp() + super().setUp() self.qos_mock.get = ( - volume_fakes.FakeQos.get_qoses(self.qos_specs)) + volume_fakes.get_qoses(self.qos_specs)) # Get the command object to test self.cmd = qos_specs.DeleteQos(self.app, None) @@ -263,11 +263,11 @@ def test_delete_multiple_qoses_with_exception(self): class TestQosDisassociate(TestQos): - volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() - qos_spec = volume_fakes.FakeQos.create_one_qos() + volume_type = volume_fakes.create_one_volume_type() + qos_spec = volume_fakes.create_one_qos() def setUp(self): - super(TestQosDisassociate, self).setUp() + super().setUp() self.qos_mock.get.return_value = self.qos_spec self.types_mock.get.return_value = self.volume_type @@ -311,8 +311,8 @@ def test_qos_disassociate_with_all_volume_types(self): class TestQosList(TestQos): - qos_specs = volume_fakes.FakeQos.create_qoses(count=2) - qos_association = volume_fakes.FakeQos.create_one_qos_association() + qos_specs = volume_fakes.create_qoses(count=2) + qos_association = volume_fakes.create_one_qos_association() columns = ( 'ID', @@ -332,7 +332,7 @@ class TestQosList(TestQos): )) def setUp(self): - super(TestQosList, self).setUp() + super().setUp() self.qos_mock.list.return_value = self.qos_specs self.qos_mock.get_associations.return_value = [self.qos_association] @@ -382,10 +382,10 @@ def test_qos_list_no_association(self): class TestQosSet(TestQos): - qos_spec = volume_fakes.FakeQos.create_one_qos() + qos_spec = volume_fakes.create_one_qos() def setUp(self): - super(TestQosSet, self).setUp() + super().setUp() self.qos_mock.get.return_value = self.qos_spec # Get the command object to test @@ -414,11 +414,11 @@ def test_qos_set_with_properties_with_id(self): class TestQosShow(TestQos): - qos_spec = volume_fakes.FakeQos.create_one_qos() - qos_association = volume_fakes.FakeQos.create_one_qos_association() + qos_spec = volume_fakes.create_one_qos() + qos_association = volume_fakes.create_one_qos_association() def setUp(self): - super(TestQosShow, self).setUp() + super().setUp() self.qos_mock.get.return_value = self.qos_spec self.qos_mock.get_associations.return_value = [self.qos_association] # Get the command object to test @@ -459,10 +459,10 @@ def test_qos_show(self): class TestQosUnset(TestQos): - qos_spec = volume_fakes.FakeQos.create_one_qos() + qos_spec = volume_fakes.create_one_qos() def setUp(self): - super(TestQosUnset, self).setUp() + super().setUp() self.qos_mock.get.return_value = self.qos_spec # Get the command object to test diff --git a/openstackclient/tests/unit/volume/v1/test_service.py b/openstackclient/tests/unit/volume/v1/test_service.py index 82d21bfc42..a199c91349 100644 --- a/openstackclient/tests/unit/volume/v1/test_service.py +++ b/openstackclient/tests/unit/volume/v1/test_service.py @@ -14,14 +14,14 @@ from osc_lib import exceptions -from openstackclient.tests.unit.volume.v1 import fakes as service_fakes +from openstackclient.tests.unit.volume.v1 import fakes as volume_fakes from openstackclient.volume.v1 import service -class TestService(service_fakes.TestVolumev1): +class TestService(volume_fakes.TestVolumev1): def setUp(self): - super(TestService, self).setUp() + super().setUp() # Get a shortcut to the ServiceManager Mock self.service_mock = self.app.client_manager.volume.services @@ -31,10 +31,10 @@ def setUp(self): class TestServiceList(TestService): # The service to be listed - services = service_fakes.FakeService.create_one_service() + services = volume_fakes.create_one_service() def setUp(self): - super(TestServiceList, self).setUp() + super().setUp() self.service_mock.list.return_value = [self.services] @@ -144,10 +144,10 @@ def test_service_list_with_long_option(self): class TestServiceSet(TestService): - service = service_fakes.FakeService.create_one_service() + service = volume_fakes.create_one_service() def setUp(self): - super(TestServiceSet, self).setUp() + super().setUp() self.service_mock.enable.return_value = self.service self.service_mock.disable.return_value = self.service diff --git a/openstackclient/tests/unit/volume/v1/test_transfer_request.py b/openstackclient/tests/unit/volume/v1/test_transfer_request.py index 333bf52688..97700fbb7f 100644 --- a/openstackclient/tests/unit/volume/v1/test_transfer_request.py +++ b/openstackclient/tests/unit/volume/v1/test_transfer_request.py @@ -18,14 +18,14 @@ from osc_lib import exceptions from osc_lib import utils -from openstackclient.tests.unit.volume.v1 import fakes as transfer_fakes +from openstackclient.tests.unit.volume.v1 import fakes as volume_fakes from openstackclient.volume.v1 import volume_transfer_request -class TestTransfer(transfer_fakes.TestVolumev1): +class TestTransfer(volume_fakes.TestVolumev1): def setUp(self): - super(TestTransfer, self).setUp() + super().setUp() # Get a shortcut to the TransferManager Mock self.transfer_mock = self.app.client_manager.volume.transfers @@ -45,10 +45,9 @@ class TestTransferAccept(TestTransfer): ) def setUp(self): - super(TestTransferAccept, self).setUp() + super().setUp() - self.volume_transfer = ( - transfer_fakes.FakeTransfer.create_one_transfer()) + self.volume_transfer = volume_fakes.create_one_transfer() self.data = ( self.volume_transfer.id, self.volume_transfer.name, @@ -103,7 +102,7 @@ def test_transfer_accept_no_option(self): class TestTransferCreate(TestTransfer): - volume = transfer_fakes.FakeVolume.create_one_volume() + volume = volume_fakes.create_one_volume() columns = ( 'auth_key', @@ -114,12 +113,14 @@ class TestTransferCreate(TestTransfer): ) def setUp(self): - super(TestTransferCreate, self).setUp() - - self.volume_transfer = transfer_fakes.FakeTransfer.create_one_transfer( - attrs={'volume_id': self.volume.id, - 'auth_key': 'key', - 'created_at': 'time'} + super().setUp() + + self.volume_transfer = volume_fakes.create_one_transfer( + attrs={ + 'volume_id': self.volume.id, + 'auth_key': 'key', + 'created_at': 'time', + }, ) self.data = ( self.volume_transfer.auth_key, @@ -173,13 +174,14 @@ def test_transfer_create_with_name(self): class TestTransferDelete(TestTransfer): - volume_transfers = transfer_fakes.FakeTransfer.create_transfers(count=2) + volume_transfers = volume_fakes.create_transfers(count=2) def setUp(self): - super(TestTransferDelete, self).setUp() + super().setUp() - self.transfer_mock.get = ( - transfer_fakes.FakeTransfer.get_transfers(self.volume_transfers)) + self.transfer_mock.get = volume_fakes.get_transfers( + self.volume_transfers, + ) self.transfer_mock.delete.return_value = None # Get the command object to mock @@ -252,10 +254,10 @@ def test_delete_multiple_transfers_with_exception(self): class TestTransferList(TestTransfer): # The Transfers to be listed - volume_transfers = transfer_fakes.FakeTransfer.create_one_transfer() + volume_transfers = volume_fakes.create_one_transfer() def setUp(self): - super(TestTransferList, self).setUp() + super().setUp() self.transfer_mock.list.return_value = [self.volume_transfers] @@ -346,11 +348,10 @@ class TestTransferShow(TestTransfer): ) def setUp(self): - super(TestTransferShow, self).setUp() + super().setUp() - self.volume_transfer = ( - transfer_fakes.FakeTransfer.create_one_transfer( - attrs={'created_at': 'time'}) + self.volume_transfer = volume_fakes.create_one_transfer( + attrs={'created_at': 'time'} ) self.data = ( self.volume_transfer.created_at, diff --git a/openstackclient/tests/unit/volume/v1/test_type.py b/openstackclient/tests/unit/volume/v1/test_type.py index ca74c3e68d..c878824901 100644 --- a/openstackclient/tests/unit/volume/v1/test_type.py +++ b/openstackclient/tests/unit/volume/v1/test_type.py @@ -27,7 +27,7 @@ class TestType(volume_fakes.TestVolumev1): def setUp(self): - super(TestType, self).setUp() + super().setUp() self.types_mock = self.app.client_manager.volume.volume_types self.types_mock.reset_mock() @@ -47,11 +47,11 @@ class TestTypeCreate(TestType): ) def setUp(self): - super(TestTypeCreate, self).setUp() + super().setUp() - self.new_volume_type = \ - volume_fakes.FakeVolumeType.create_one_volume_type( - methods={'set_keys': {'myprop': 'myvalue'}}) + self.new_volume_type = volume_fakes.create_one_volume_type( + methods={'set_keys': {'myprop': 'myvalue'}}, + ) self.data = ( self.new_volume_type.description, self.new_volume_type.id, @@ -87,12 +87,12 @@ def test_type_create_with_encryption(self): 'key_size': '128', 'control_location': 'front-end', } - encryption_type = \ - volume_fakes.FakeVolumeType.create_one_encryption_volume_type( - attrs=encryption_info) - self.new_volume_type = \ - volume_fakes.FakeVolumeType.create_one_volume_type( - attrs={'encryption': encryption_info}) + encryption_type = volume_fakes.create_one_encryption_volume_type( + attrs=encryption_info, + ) + self.new_volume_type = volume_fakes.create_one_volume_type( + attrs={'encryption': encryption_info}, + ) self.types_mock.create.return_value = self.new_volume_type self.encryption_types_mock.create.return_value = encryption_type encryption_columns = ( @@ -145,13 +145,12 @@ def test_type_create_with_encryption(self): class TestTypeDelete(TestType): - volume_types = volume_fakes.FakeVolumeType.create_volume_types(count=2) + volume_types = volume_fakes.create_volume_types(count=2) def setUp(self): - super(TestTypeDelete, self).setUp() + super().setUp() - self.types_mock.get = volume_fakes.FakeVolumeType.get_volume_types( - self.volume_types) + self.types_mock.get = volume_fakes.get_volume_types(self.volume_types) self.types_mock.delete.return_value = None # Get the command object to mock @@ -221,7 +220,7 @@ def test_delete_multiple_types_with_exception(self): class TestTypeList(TestType): - volume_types = volume_fakes.FakeVolumeType.create_volume_types() + volume_types = volume_fakes.create_volume_types() columns = [ "ID", @@ -252,7 +251,7 @@ class TestTypeList(TestType): )) def setUp(self): - super(TestTypeList, self).setUp() + super().setUp() self.types_mock.list.return_value = self.volume_types self.encryption_types_mock.create.return_value = None @@ -288,9 +287,9 @@ def test_type_list_with_options(self): self.assertCountEqual(self.data_long, list(data)) def test_type_list_with_encryption(self): - encryption_type = \ - volume_fakes.FakeVolumeType.create_one_encryption_volume_type( - attrs={'volume_type_id': self.volume_types[0].id}) + encryption_type = volume_fakes.create_one_encryption_volume_type( + attrs={'volume_type_id': self.volume_types[0].id}, + ) encryption_info = { 'provider': 'LuksEncryptor', 'cipher': None, @@ -335,11 +334,12 @@ def test_type_list_with_encryption(self): class TestTypeSet(TestType): - volume_type = volume_fakes.FakeVolumeType.create_one_volume_type( - methods={'set_keys': None}) + volume_type = volume_fakes.create_one_volume_type( + methods={'set_keys': None}, + ) def setUp(self): - super(TestTypeSet, self).setUp() + super().setUp() self.types_mock.get.return_value = self.volume_type @@ -441,9 +441,9 @@ class TestTypeShow(TestType): ) def setUp(self): - super(TestTypeShow, self).setUp() + super().setUp() - self.volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() + self.volume_type = volume_fakes.create_one_volume_type() self.data = ( self.volume_type.description, self.volume_type.id, @@ -474,16 +474,16 @@ def test_type_show(self): self.assertCountEqual(self.data, data) def test_type_show_with_encryption(self): - encryption_type = \ - volume_fakes.FakeVolumeType.create_one_encryption_volume_type() + encryption_type = volume_fakes.create_one_encryption_volume_type() encryption_info = { 'provider': 'LuksEncryptor', 'cipher': None, 'key_size': None, 'control_location': 'front-end', } - self.volume_type = volume_fakes.FakeVolumeType.create_one_volume_type( - attrs={'encryption': encryption_info}) + self.volume_type = volume_fakes.create_one_volume_type( + attrs={'encryption': encryption_info}, + ) self.types_mock.get.return_value = self.volume_type self.encryption_types_mock.get.return_value = encryption_type encryption_columns = ( @@ -521,11 +521,12 @@ def test_type_show_with_encryption(self): class TestTypeUnset(TestType): - volume_type = volume_fakes.FakeVolumeType.create_one_volume_type( - methods={'unset_keys': None}) + volume_type = volume_fakes.create_one_volume_type( + methods={'unset_keys': None}, + ) def setUp(self): - super(TestTypeUnset, self).setUp() + super().setUp() self.types_mock.get.return_value = self.volume_type @@ -599,7 +600,7 @@ def test_type_unset_encryption_type(self): class TestColumns(TestType): def test_encryption_info_column_with_info(self): - fake_volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() + fake_volume_type = volume_fakes.create_one_volume_type() type_id = fake_volume_type.id encryption_info = { @@ -615,7 +616,7 @@ def test_encryption_info_column_with_info(self): self.assertEqual(encryption_info, col.machine_readable()) def test_encryption_info_column_without_info(self): - fake_volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() + fake_volume_type = volume_fakes.create_one_volume_type() type_id = fake_volume_type.id col = volume_type.EncryptionInfoColumn(type_id, {}) diff --git a/openstackclient/tests/unit/volume/v1/test_volume.py b/openstackclient/tests/unit/volume/v1/test_volume.py index 584eca2a7b..9f16b398ec 100644 --- a/openstackclient/tests/unit/volume/v1/test_volume.py +++ b/openstackclient/tests/unit/volume/v1/test_volume.py @@ -31,7 +31,7 @@ class TestVolume(volume_fakes.TestVolumev1): def setUp(self): - super(TestVolume, self).setUp() + super().setUp() # Get a shortcut to the VolumeManager Mock self.volumes_mock = self.app.client_manager.volume.volumes @@ -50,11 +50,9 @@ def setUp(self): self.images_mock.reset_mock() def setup_volumes_mock(self, count): - volumes = volume_fakes.FakeVolume.create_volumes(count=count) + volumes = volume_fakes.create_volumes(count=count) - self.volumes_mock.get = volume_fakes.FakeVolume.get_volumes( - volumes, - 0) + self.volumes_mock.get = volume_fakes.get_volumes(volumes, 0) return volumes @@ -79,8 +77,8 @@ class TestVolumeCreate(TestVolume): ) def setUp(self): - super(TestVolumeCreate, self).setUp() - self.new_volume = volume_fakes.FakeVolume.create_one_volume() + super().setUp() + self.new_volume = volume_fakes.create_one_volume() self.datalist = ( self.new_volume.attachments, self.new_volume.availability_zone, @@ -635,7 +633,7 @@ def test_volume_create_backward_compatibility(self): class TestVolumeDelete(TestVolume): def setUp(self): - super(TestVolumeDelete, self).setUp() + super().setUp() self.volumes_mock.delete.return_value = None @@ -725,7 +723,7 @@ def test_volume_delete_with_force(self): class TestVolumeList(TestVolume): - _volume = volume_fakes.FakeVolume.create_one_volume() + _volume = volume_fakes.create_one_volume() columns = ( 'ID', 'Name', @@ -744,7 +742,7 @@ class TestVolumeList(TestVolume): ) def setUp(self): - super(TestVolumeList, self).setUp() + super().setUp() self.volumes_mock.list.return_value = [self._volume] @@ -921,10 +919,10 @@ def test_volume_list_backward_compatibility(self): class TestVolumeMigrate(TestVolume): - _volume = volume_fakes.FakeVolume.create_one_volume() + _volume = volume_fakes.create_one_volume() def setUp(self): - super(TestVolumeMigrate, self).setUp() + super().setUp() self.volumes_mock.get.return_value = self._volume self.volumes_mock.migrate_volume.return_value = None @@ -983,10 +981,10 @@ def test_volume_migrate_without_host(self): class TestVolumeSet(TestVolume): - _volume = volume_fakes.FakeVolume.create_one_volume() + _volume = volume_fakes.create_one_volume() def setUp(self): - super(TestVolumeSet, self).setUp() + super().setUp() self.volumes_mock.get.return_value = self._volume @@ -1243,8 +1241,8 @@ class TestVolumeShow(TestVolume): ) def setUp(self): - super(TestVolumeShow, self).setUp() - self._volume = volume_fakes.FakeVolume.create_one_volume() + super().setUp() + self._volume = volume_fakes.create_one_volume() self.datalist = ( self._volume.attachments, self._volume.availability_zone, @@ -1300,10 +1298,10 @@ def test_volume_show_backward_compatibility(self): class TestVolumeUnset(TestVolume): - _volume = volume_fakes.FakeVolume.create_one_volume() + _volume = volume_fakes.create_one_volume() def setUp(self): - super(TestVolumeUnset, self).setUp() + super().setUp() self.volumes_mock.get.return_value = self._volume @@ -1346,7 +1344,7 @@ def test_volume_unset_property(self): class TestColumns(TestVolume): def test_attachments_column_without_server_cache(self): - _volume = volume_fakes.FakeVolume.create_one_volume() + _volume = volume_fakes.create_one_volume() server_id = _volume.attachments[0]['server_id'] device = _volume.attachments[0]['device'] @@ -1356,7 +1354,7 @@ def test_attachments_column_without_server_cache(self): self.assertEqual(_volume.attachments, col.machine_readable()) def test_attachments_column_with_server_cache(self): - _volume = volume_fakes.FakeVolume.create_one_volume() + _volume = volume_fakes.create_one_volume() server_id = _volume.attachments[0]['server_id'] device = _volume.attachments[0]['device'] diff --git a/openstackclient/tests/unit/volume/v1/test_volume_backup.py b/openstackclient/tests/unit/volume/v1/test_volume_backup.py index 2f568929ad..b705b4b9b9 100644 --- a/openstackclient/tests/unit/volume/v1/test_volume_backup.py +++ b/openstackclient/tests/unit/volume/v1/test_volume_backup.py @@ -25,7 +25,7 @@ class TestBackup(volume_fakes.TestVolumev1): def setUp(self): - super(TestBackup, self).setUp() + super().setUp() self.backups_mock = self.app.client_manager.volume.backups self.backups_mock.reset_mock() @@ -39,7 +39,7 @@ def setUp(self): class TestBackupCreate(TestBackup): - volume = volume_fakes.FakeVolume.create_one_volume() + volume = volume_fakes.create_one_volume() columns = ( 'availability_zone', @@ -55,9 +55,10 @@ class TestBackupCreate(TestBackup): ) def setUp(self): - super(TestBackupCreate, self).setUp() - self.new_backup = volume_fakes.FakeBackup.create_one_backup( - attrs={'volume_id': self.volume.id}) + super().setUp() + self.new_backup = volume_fakes.create_one_backup( + attrs={'volume_id': self.volume.id}, + ) self.data = ( self.new_backup.availability_zone, self.new_backup.container, @@ -129,13 +130,12 @@ def test_backup_create_without_name(self): class TestBackupDelete(TestBackup): - backups = volume_fakes.FakeBackup.create_backups(count=2) + backups = volume_fakes.create_backups(count=2) def setUp(self): - super(TestBackupDelete, self).setUp() + super().setUp() - self.backups_mock.get = ( - volume_fakes.FakeBackup.get_backups(self.backups)) + self.backups_mock.get = volume_fakes.get_backups(self.backups) self.backups_mock.delete.return_value = None # Get the command object to mock @@ -205,9 +205,11 @@ def test_delete_multiple_backups_with_exception(self): class TestBackupList(TestBackup): - volume = volume_fakes.FakeVolume.create_one_volume() - backups = volume_fakes.FakeBackup.create_backups( - attrs={'volume_id': volume.display_name}, count=3) + volume = volume_fakes.create_one_volume() + backups = volume_fakes.create_backups( + attrs={'volume_id': volume.display_name}, + count=3, + ) columns = [ 'ID', @@ -245,7 +247,7 @@ class TestBackupList(TestBackup): )) def setUp(self): - super(TestBackupList, self).setUp() + super().setUp() self.volumes_mock.list.return_value = [self.volume] self.backups_mock.list.return_value = self.backups @@ -314,9 +316,10 @@ def test_backup_list_with_options(self): class TestBackupRestore(TestBackup): - volume = volume_fakes.FakeVolume.create_one_volume() - backup = volume_fakes.FakeBackup.create_one_backup( - attrs={'volume_id': volume.id}) + volume = volume_fakes.create_one_volume() + backup = volume_fakes.create_one_backup( + attrs={'volume_id': volume.id}, + ) def setUp(self): super().setUp() @@ -324,7 +327,7 @@ def setUp(self): self.backups_mock.get.return_value = self.backup self.volumes_mock.get.return_value = self.volume self.restores_mock.restore.return_value = ( - volume_fakes.FakeVolume.create_one_volume( + volume_fakes.create_one_volume( {'id': self.volume['id']}, ) ) @@ -400,8 +403,8 @@ class TestBackupShow(TestBackup): ) def setUp(self): - super(TestBackupShow, self).setUp() - self.backup = volume_fakes.FakeBackup.create_one_backup() + super().setUp() + self.backup = volume_fakes.create_one_backup() self.data = ( self.backup.availability_zone, self.backup.container, diff --git a/openstackclient/tests/unit/volume/v2/fakes.py b/openstackclient/tests/unit/volume/v2/fakes.py index 33588e045c..a3ef142f27 100644 --- a/openstackclient/tests/unit/volume/v2/fakes.py +++ b/openstackclient/tests/unit/volume/v2/fakes.py @@ -41,7 +41,6 @@ class FakeVolumeClient: - def __init__(self, **kwargs): self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] @@ -76,8 +75,9 @@ def __init__(self, **kwargs): self.transfers = mock.Mock() self.transfers.resource_class = fakes.FakeResource(None, {}) self.volume_encryption_types = mock.Mock() - self.volume_encryption_types.resource_class = ( - fakes.FakeResource(None, {})) + self.volume_encryption_types.resource_class = fakes.FakeResource( + None, {} + ) self.volume_snapshots = mock.Mock() self.volume_snapshots.resource_class = fakes.FakeResource(None, {}) self.volume_type_access = mock.Mock() @@ -89,1138 +89,1047 @@ def __init__(self, **kwargs): class TestVolume(utils.TestCommand): - def setUp(self): super().setUp() self.app.client_manager.volume = FakeVolumeClient( - endpoint=fakes.AUTH_URL, - token=fakes.AUTH_TOKEN + endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN ) self.app.client_manager.identity = identity_fakes.FakeIdentityv3Client( - endpoint=fakes.AUTH_URL, - token=fakes.AUTH_TOKEN + endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN ) self.app.client_manager.image = image_fakes.FakeImagev2Client( - endpoint=fakes.AUTH_URL, - token=fakes.AUTH_TOKEN + endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN ) -class FakeTransfer(object): - """Fake one or more Transfer.""" - - @staticmethod - def create_one_transfer(attrs=None): - """Create a fake transfer. - - :param dict attrs: - A dictionary with all attributes of Transfer Request - :return: - A FakeResource object with volume_id, name, id. - """ - # Set default attribute - transfer_info = { - 'volume_id': 'volume-id-' + uuid.uuid4().hex, - 'name': 'fake_transfer_name', - 'id': 'id-' + uuid.uuid4().hex, - 'links': 'links-' + uuid.uuid4().hex, - } - - # Overwrite default attributes if there are some attributes set - attrs = attrs or {} - - transfer_info.update(attrs) - - transfer = fakes.FakeResource( - None, - transfer_info, - loaded=True) - - return transfer - - @staticmethod - def create_transfers(attrs=None, count=2): - """Create multiple fake transfers. - - :param dict attrs: - A dictionary with all attributes of transfer - :param Integer count: - The number of transfers to be faked - :return: - A list of FakeResource objects - """ - transfers = [] - for n in range(0, count): - transfers.append(FakeTransfer.create_one_transfer(attrs)) - - return transfers - - @staticmethod - def get_transfers(transfers=None, count=2): - """Get an iterable MagicMock object with a list of faked transfers. - - If transfers list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param List transfers: - A list of FakeResource objects faking transfers - :param Integer count: - The number of transfers to be faked - :return - An iterable Mock object with side_effect set to a list of faked - transfers - """ - if transfers is None: - transfers = FakeTransfer.create_transfers(count) - - return mock.Mock(side_effect=transfers) - - -class FakeTypeAccess(object): - """Fake one or more volume type access.""" - - @staticmethod - def create_one_type_access(attrs=None): - """Create a fake volume type access for project. - - :param dict attrs: - A dictionary with all attributes - :return: - A FakeResource object, with Volume_type_ID and Project_ID. - """ - if attrs is None: - attrs = {} - - # Set default attributes. - type_access_attrs = { - 'volume_type_id': 'volume-type-id-' + uuid.uuid4().hex, - 'project_id': 'project-id-' + uuid.uuid4().hex, - } - - # Overwrite default attributes. - type_access_attrs.update(attrs) - - type_access = fakes.FakeResource( - None, - type_access_attrs, - loaded=True) - - return type_access - - -class FakeService(object): - """Fake one or more Services.""" - - @staticmethod - def create_one_service(attrs=None): - """Create a fake service. - - :param dict attrs: - A dictionary with all attributes of service - :return: - A FakeResource object with host, status, etc. - """ - # Set default attribute - service_info = { - 'host': 'host_test', - 'binary': 'cinder_test', - 'status': 'enabled', - 'disabled_reason': 'LongHoliday-GoldenWeek', - 'zone': 'fake_zone', - 'updated_at': 'fake_date', - 'state': 'fake_state', - } - - # Overwrite default attributes if there are some attributes set - attrs = attrs or {} - - service_info.update(attrs) - - service = fakes.FakeResource( - None, - service_info, - loaded=True) - - return service - - @staticmethod - def create_services(attrs=None, count=2): - """Create multiple fake services. - - :param dict attrs: - A dictionary with all attributes of service - :param Integer count: - The number of services to be faked - :return: - A list of FakeResource objects - """ - services = [] - for n in range(0, count): - services.append(FakeService.create_one_service(attrs)) - - return services - - -class FakeCapability(object): - """Fake capability.""" - - @staticmethod - def create_one_capability(attrs=None): - """Create a fake volume backend capability. - - :param dict attrs: - A dictionary with all attributes of the Capabilities. - :return: - A FakeResource object with capability name and attrs. - """ - # Set default attribute - capability_info = { - "namespace": "OS::Storage::Capabilities::fake", - "vendor_name": "OpenStack", - "volume_backend_name": "lvmdriver-1", - "pool_name": "pool", - "driver_version": "2.0.0", - "storage_protocol": "iSCSI", - "display_name": "Capabilities of Cinder LVM driver", - "description": "Blah, blah.", - "visibility": "public", - "replication_targets": [], - "properties": { - "compression": { - "title": "Compression", - "description": "Enables compression.", - "type": "boolean" - }, - "qos": { - "title": "QoS", - "description": "Enables QoS.", - "type": "boolean" - }, - "replication": { - "title": "Replication", - "description": "Enables replication.", - "type": "boolean" - }, - "thin_provisioning": { - "title": "Thin Provisioning", - "description": "Sets thin provisioning.", - "type": "boolean" - } - } - } +def create_one_transfer(attrs=None): + """Create a fake transfer. - # Overwrite default attributes if there are some attributes set - capability_info.update(attrs or {}) - - capability = fakes.FakeResource( - None, - capability_info, - loaded=True) - - return capability - - -class FakePool(object): - """Fake Pools.""" - - @staticmethod - def create_one_pool(attrs=None): - """Create a fake pool. - - :param dict attrs: - A dictionary with all attributes of the pool - :return: - A FakeResource object with pool name and attrs. - """ - # Set default attribute - pool_info = { - 'name': 'host@lvmdriver-1#lvmdriver-1', - 'storage_protocol': 'iSCSI', - 'thick_provisioning_support': False, - 'thin_provisioning_support': True, - 'total_volumes': 99, - 'total_capacity_gb': 1000.00, - 'allocated_capacity_gb': 100, - 'max_over_subscription_ratio': 200.0, - } - - # Overwrite default attributes if there are some attributes set - pool_info.update(attrs or {}) - - pool = fakes.FakeResource( - None, - pool_info, - loaded=True) - - return pool - - -class FakeVolume(object): - """Fake one or more volumes.""" - - @staticmethod - def create_one_volume(attrs=None): - """Create a fake volume. - - :param dict attrs: - A dictionary with all attributes of volume - :return: - A FakeResource object with id, name, status, etc. - """ - attrs = attrs or {} - - # Set default attribute - volume_info = { - 'id': 'volume-id' + uuid.uuid4().hex, - 'name': 'volume-name' + uuid.uuid4().hex, - 'description': 'description' + uuid.uuid4().hex, - 'status': random.choice(['available', 'in_use']), - 'size': random.randint(1, 20), - 'volume_type': - random.choice(['fake_lvmdriver-1', 'fake_lvmdriver-2']), - 'bootable': - random.randint(0, 1), - 'metadata': { - 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, - 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, - 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex}, - 'snapshot_id': random.randint(1, 5), - 'availability_zone': 'zone' + uuid.uuid4().hex, - 'attachments': [{ - 'device': '/dev/' + uuid.uuid4().hex, - 'server_id': uuid.uuid4().hex, - }, ], - } + :param dict attrs: + A dictionary with all attributes of Transfer Request + :return: + A FakeResource object with volume_id, name, id. + """ + # Set default attribute + transfer_info = { + 'volume_id': 'volume-id-' + uuid.uuid4().hex, + 'name': 'fake_transfer_name', + 'id': 'id-' + uuid.uuid4().hex, + 'links': 'links-' + uuid.uuid4().hex, + } - # Overwrite default attributes if there are some attributes set - volume_info.update(attrs) - - volume = fakes.FakeResource( - None, - volume_info, - loaded=True) - return volume - - @staticmethod - def create_volumes(attrs=None, count=2): - """Create multiple fake volumes. - - :param dict attrs: - A dictionary with all attributes of volume - :param Integer count: - The number of volumes to be faked - :return: - A list of FakeResource objects - """ - volumes = [] - for n in range(0, count): - volumes.append(FakeVolume.create_one_volume(attrs)) - - return volumes - - @staticmethod - def create_one_sdk_volume(attrs=None): - """Create a fake volume. - - :param dict attrs: - A dictionary with all attributes of volume - :return: - A FakeResource object with id, name, status, etc. - """ - attrs = attrs or {} - - # Set default attribute - volume_info = { - 'id': 'volume-id' + uuid.uuid4().hex, - 'name': 'volume-name' + uuid.uuid4().hex, - 'description': 'description' + uuid.uuid4().hex, - 'status': random.choice(['available', 'in_use']), - 'size': random.randint(1, 20), - 'volume_type': - random.choice(['fake_lvmdriver-1', 'fake_lvmdriver-2']), - 'bootable': - random.choice(['true', 'false']), - 'metadata': { - 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, - 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, - 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex}, - 'snapshot_id': random.randint(1, 5), - 'availability_zone': 'zone' + uuid.uuid4().hex, - 'attachments': [{ - 'device': '/dev/' + uuid.uuid4().hex, - 'server_id': uuid.uuid4().hex, - }, ], - } + # Overwrite default attributes if there are some attributes set + attrs = attrs or {} - # Overwrite default attributes if there are some attributes set - volume_info.update(attrs) - return volume.Volume(**volume_info) - - @staticmethod - def create_sdk_volumes(attrs=None, count=2): - """Create multiple fake volumes. - - :param dict attrs: - A dictionary with all attributes of volume - :param Integer count: - The number of volumes to be faked - :return: - A list of FakeResource objects - """ - volumes = [] - for n in range(0, count): - volumes.append(FakeVolume.create_one_sdk_volume(attrs)) - - return volumes - - @staticmethod - def get_volumes(volumes=None, count=2): - """Get an iterable MagicMock object with a list of faked volumes. - - If volumes list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param List volumes: - A list of FakeResource objects faking volumes - :param Integer count: - The number of volumes to be faked - :return - An iterable Mock object with side_effect set to a list of faked - volumes - """ - if volumes is None: - volumes = FakeVolume.create_volumes(count) - - return mock.Mock(side_effect=volumes) - - @staticmethod - def get_volume_columns(volume=None): - """Get the volume columns from a faked volume object. - - :param volume: - A FakeResource objects faking volume - :return - A tuple which may include the following keys: - ('id', 'name', 'description', 'status', 'size', 'volume_type', - 'metadata', 'snapshot', 'availability_zone', 'attachments') - """ - if volume is not None: - return tuple(k for k in sorted(volume.keys())) - return tuple([]) - - @staticmethod - def get_volume_data(volume=None): - """Get the volume data from a faked volume object. - - :param volume: - A FakeResource objects faking volume - :return - A tuple which may include the following values: - ('ce26708d', 'fake_volume', 'fake description', 'available', - 20, 'fake_lvmdriver-1', "Alpha='a', Beta='b', Gamma='g'", - 1, 'nova', [{'device': '/dev/ice', 'server_id': '1233'}]) - """ - data_list = [] - if volume is not None: - for x in sorted(volume.keys()): - if x == 'tags': - # The 'tags' should be format_list - data_list.append( - format_columns.ListColumn(volume.info.get(x))) - else: - data_list.append(volume.info.get(x)) - return tuple(data_list) - - -class FakeAvailabilityZone(object): - """Fake one or more volume availability zones (AZs).""" - - @staticmethod - def create_one_availability_zone(attrs=None): - """Create a fake AZ. - - :param dict attrs: - A dictionary with all attributes - :return: - A FakeResource object with zoneName, zoneState, etc. - """ - attrs = attrs or {} - - # Set default attributes. - availability_zone = { - 'zoneName': uuid.uuid4().hex, - 'zoneState': {'available': True}, - } + transfer_info.update(attrs) - # Overwrite default attributes. - availability_zone.update(attrs) - - availability_zone = fakes.FakeResource( - info=copy.deepcopy(availability_zone), - loaded=True) - return availability_zone - - @staticmethod - def create_availability_zones(attrs=None, count=2): - """Create multiple fake AZs. - - :param dict attrs: - A dictionary with all attributes - :param int count: - The number of AZs to fake - :return: - A list of FakeResource objects faking the AZs - """ - availability_zones = [] - for i in range(0, count): - availability_zone = \ - FakeAvailabilityZone.create_one_availability_zone(attrs) - availability_zones.append(availability_zone) - - return availability_zones - - -class FakeBackup(object): - """Fake one or more backup.""" - - @staticmethod - def create_one_backup(attrs=None): - """Create a fake backup. - - :param dict attrs: - A dictionary with all attributes - :return: - A FakeResource object with id, name, volume_id, etc. - """ - attrs = attrs or {} - - # Set default attributes. - backup_info = { - "id": 'backup-id-' + uuid.uuid4().hex, - "name": 'backup-name-' + uuid.uuid4().hex, - "volume_id": 'volume-id-' + uuid.uuid4().hex, - "snapshot_id": 'snapshot-id' + uuid.uuid4().hex, - "description": 'description-' + uuid.uuid4().hex, - "object_count": None, - "container": 'container-' + uuid.uuid4().hex, - "size": random.randint(1, 20), - "status": "error", - "availability_zone": 'zone' + uuid.uuid4().hex, - } + transfer = fakes.FakeResource(None, transfer_info, loaded=True) - # Overwrite default attributes. - backup_info.update(attrs) - - backup = fakes.FakeResource( - info=copy.deepcopy(backup_info), - loaded=True) - return backup - - @staticmethod - def create_backups(attrs=None, count=2): - """Create multiple fake backups. - - :param dict attrs: - A dictionary with all attributes - :param int count: - The number of backups to fake - :return: - A list of FakeResource objects faking the backups - """ - backups = [] - for i in range(0, count): - backup = FakeBackup.create_one_backup(attrs) - backups.append(backup) - - return backups - - @staticmethod - def get_backups(backups=None, count=2): - """Get an iterable MagicMock object with a list of faked backups. - - If backups list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param List backups: - A list of FakeResource objects faking backups - :param Integer count: - The number of backups to be faked - :return - An iterable Mock object with side_effect set to a list of faked - backups - """ - if backups is None: - backups = FakeBackup.create_backups(count) - - return mock.Mock(side_effect=backups) - - @staticmethod - def create_backup_record(): - """Gets a fake backup record for a given backup. - - :return: An "exported" backup record. - """ - - return { - 'backup_service': 'cinder.backup.drivers.swift.SwiftBackupDriver', - 'backup_url': 'eyJzdGF0dXMiOiAiYXZh', - } + return transfer - @staticmethod - def import_backup_record(): - """Creates a fake backup record import response from a backup. - - :return: The fake backup object that was encoded. - """ - return { - 'backup': { - 'id': 'backup.id', - 'name': 'backup.name', - 'links': [ - {'href': 'link1', 'rel': 'self'}, - {'href': 'link2', 'rel': 'bookmark'}, - ], - }, - } +def create_transfers(attrs=None, count=2): + """Create multiple fake transfers. -class FakeConsistencyGroup(object): - """Fake one or more consistency group.""" - - @staticmethod - def create_one_consistency_group(attrs=None): - """Create a fake consistency group. - - :param dict attrs: - A dictionary with all attributes - :return: - A FakeResource object with id, name, description, etc. - """ - attrs = attrs or {} - - # Set default attributes. - consistency_group_info = { - "id": 'backup-id-' + uuid.uuid4().hex, - "name": 'backup-name-' + uuid.uuid4().hex, - "description": 'description-' + uuid.uuid4().hex, - "status": "error", - "availability_zone": 'zone' + uuid.uuid4().hex, - "created_at": 'time-' + uuid.uuid4().hex, - "volume_types": ['volume-type1'], - } + :param dict attrs: + A dictionary with all attributes of transfer + :param Integer count: + The number of transfers to be faked + :return: + A list of FakeResource objects + """ + transfers = [] + for n in range(0, count): + transfers.append(create_one_transfer(attrs)) - # Overwrite default attributes. - consistency_group_info.update(attrs) - - consistency_group = fakes.FakeResource( - info=copy.deepcopy(consistency_group_info), - loaded=True) - return consistency_group - - @staticmethod - def create_consistency_groups(attrs=None, count=2): - """Create multiple fake consistency groups. - - :param dict attrs: - A dictionary with all attributes - :param int count: - The number of consistency groups to fake - :return: - A list of FakeResource objects faking the consistency groups - """ - consistency_groups = [] - for i in range(0, count): - consistency_group = ( - FakeConsistencyGroup.create_one_consistency_group(attrs)) - consistency_groups.append(consistency_group) - - return consistency_groups - - @staticmethod - def get_consistency_groups(consistency_groups=None, count=2): - """Note: - - Get an iterable MagicMock object with a list of faked - consistency_groups. - - If consistency_groups list is provided, then initialize - the Mock object with the list. Otherwise create one. - - :param List consistency_groups: - A list of FakeResource objects faking consistency_groups - :param Integer count: - The number of consistency_groups to be faked - :return - An iterable Mock object with side_effect set to a list of faked - consistency_groups - """ - if consistency_groups is None: - consistency_groups = (FakeConsistencyGroup. - create_consistency_groups(count)) - - return mock.Mock(side_effect=consistency_groups) - - -class FakeConsistencyGroupSnapshot(object): - """Fake one or more consistency group snapshot.""" - - @staticmethod - def create_one_consistency_group_snapshot(attrs=None): - """Create a fake consistency group snapshot. - - :param dict attrs: - A dictionary with all attributes - :return: - A FakeResource object with id, name, description, etc. - """ - attrs = attrs or {} - - # Set default attributes. - consistency_group_snapshot_info = { - "id": 'id-' + uuid.uuid4().hex, - "name": 'backup-name-' + uuid.uuid4().hex, - "description": 'description-' + uuid.uuid4().hex, - "status": "error", - "consistencygroup_id": 'consistency-group-id' + uuid.uuid4().hex, - "created_at": 'time-' + uuid.uuid4().hex, - } + return transfers - # Overwrite default attributes. - consistency_group_snapshot_info.update(attrs) - - consistency_group_snapshot = fakes.FakeResource( - info=copy.deepcopy(consistency_group_snapshot_info), - loaded=True) - return consistency_group_snapshot - - @staticmethod - def create_consistency_group_snapshots(attrs=None, count=2): - """Create multiple fake consistency group snapshots. - - :param dict attrs: - A dictionary with all attributes - :param int count: - The number of consistency group snapshots to fake - :return: - A list of FakeResource objects faking the - consistency group snapshots - """ - consistency_group_snapshots = [] - for i in range(0, count): - consistency_group_snapshot = ( - FakeConsistencyGroupSnapshot. - create_one_consistency_group_snapshot(attrs) - ) - consistency_group_snapshots.append(consistency_group_snapshot) - - return consistency_group_snapshots - - @staticmethod - def get_consistency_group_snapshots(snapshots=None, count=2): - """Get an iterable MagicMock object with a list of faked cgsnapshots. - - If consistenct group snapshots list is provided, then initialize - the Mock object with the list. Otherwise create one. - - :param List snapshots: - A list of FakeResource objects faking consistency group snapshots - :param Integer count: - The number of consistency group snapshots to be faked - :return - An iterable Mock object with side_effect set to a list of faked - consistency groups - """ - if snapshots is None: - snapshots = (FakeConsistencyGroupSnapshot. - create_consistency_group_snapshots(count)) - - return mock.Mock(side_effect=snapshots) - - -class FakeExtension(object): - """Fake one or more extension.""" - - @staticmethod - def create_one_extension(attrs=None): - """Create a fake extension. - - :param dict attrs: - A dictionary with all attributes - :return: - A FakeResource object with name, namespace, etc. - """ - attrs = attrs or {} - - # Set default attributes. - extension_info = { - 'name': 'name-' + uuid.uuid4().hex, - 'namespace': ('http://docs.openstack.org/' - 'block-service/ext/scheduler-hints/api/v2'), - 'description': 'description-' + uuid.uuid4().hex, - 'updated': '2013-04-18T00:00:00+00:00', - 'alias': 'OS-SCH-HNT', - 'links': ('[{"href":' - '"https://github.com/openstack/block-api", "type":' - ' "text/html", "rel": "describedby"}]'), - } - # Overwrite default attributes. - extension_info.update(attrs) +def get_transfers(transfers=None, count=2): + """Get an iterable MagicMock object with a list of faked transfers. - extension = fakes.FakeResource( - info=copy.deepcopy(extension_info), - loaded=True) - return extension + If transfers list is provided, then initialize the Mock object with the + list. Otherwise create one. + :param List transfers: + A list of FakeResource objects faking transfers + :param Integer count: + The number of transfers to be faked + :return + An iterable Mock object with side_effect set to a list of faked + transfers + """ + if transfers is None: + transfers = create_transfers(count) + + return mock.Mock(side_effect=transfers) + + +def create_one_type_access(attrs=None): + """Create a fake volume type access for project. + + :param dict attrs: + A dictionary with all attributes + :return: + A FakeResource object, with Volume_type_ID and Project_ID. + """ + if attrs is None: + attrs = {} + + # Set default attributes. + type_access_attrs = { + 'volume_type_id': 'volume-type-id-' + uuid.uuid4().hex, + 'project_id': 'project-id-' + uuid.uuid4().hex, + } + + # Overwrite default attributes. + type_access_attrs.update(attrs) + + type_access = fakes.FakeResource(None, type_access_attrs, loaded=True) + + return type_access + + +def create_one_service(attrs=None): + """Create a fake service. + + :param dict attrs: + A dictionary with all attributes of service + :return: + A FakeResource object with host, status, etc. + """ + # Set default attribute + service_info = { + 'host': 'host_test', + 'binary': 'cinder_test', + 'status': 'enabled', + 'disabled_reason': 'LongHoliday-GoldenWeek', + 'zone': 'fake_zone', + 'updated_at': 'fake_date', + 'state': 'fake_state', + } + + # Overwrite default attributes if there are some attributes set + attrs = attrs or {} + + service_info.update(attrs) + + service = fakes.FakeResource(None, service_info, loaded=True) + + return service + + +def create_services(attrs=None, count=2): + """Create multiple fake services. + + :param dict attrs: + A dictionary with all attributes of service + :param Integer count: + The number of services to be faked + :return: + A list of FakeResource objects + """ + services = [] + for n in range(0, count): + services.append(create_one_service(attrs)) + + return services + + +def create_one_capability(attrs=None): + """Create a fake volume backend capability. + + :param dict attrs: + A dictionary with all attributes of the Capabilities. + :return: + A FakeResource object with capability name and attrs. + """ + # Set default attribute + capability_info = { + "namespace": "OS::Storage::Capabilities::fake", + "vendor_name": "OpenStack", + "volume_backend_name": "lvmdriver-1", + "pool_name": "pool", + "driver_version": "2.0.0", + "storage_protocol": "iSCSI", + "display_name": "Capabilities of Cinder LVM driver", + "description": "Blah, blah.", + "visibility": "public", + "replication_targets": [], + "properties": { + "compression": { + "title": "Compression", + "description": "Enables compression.", + "type": "boolean", + }, + "qos": { + "title": "QoS", + "description": "Enables QoS.", + "type": "boolean", + }, + "replication": { + "title": "Replication", + "description": "Enables replication.", + "type": "boolean", + }, + "thin_provisioning": { + "title": "Thin Provisioning", + "description": "Sets thin provisioning.", + "type": "boolean", + }, + }, + } + + # Overwrite default attributes if there are some attributes set + capability_info.update(attrs or {}) + + capability = fakes.FakeResource(None, capability_info, loaded=True) + + return capability + + +def create_one_pool(attrs=None): + """Create a fake pool. + + :param dict attrs: + A dictionary with all attributes of the pool + :return: + A FakeResource object with pool name and attrs. + """ + # Set default attribute + pool_info = { + 'name': 'host@lvmdriver-1#lvmdriver-1', + 'storage_protocol': 'iSCSI', + 'thick_provisioning_support': False, + 'thin_provisioning_support': True, + 'total_volumes': 99, + 'total_capacity_gb': 1000.00, + 'allocated_capacity_gb': 100, + 'max_over_subscription_ratio': 200.0, + } + + # Overwrite default attributes if there are some attributes set + pool_info.update(attrs or {}) + + pool = fakes.FakeResource(None, pool_info, loaded=True) + + return pool + + +def create_one_volume(attrs=None): + """Create a fake volume. + + :param dict attrs: + A dictionary with all attributes of volume + :return: + A FakeResource object with id, name, status, etc. + """ + attrs = attrs or {} + + # Set default attribute + volume_info = { + 'id': 'volume-id' + uuid.uuid4().hex, + 'name': 'volume-name' + uuid.uuid4().hex, + 'description': 'description' + uuid.uuid4().hex, + 'status': random.choice(['available', 'in_use']), + 'size': random.randint(1, 20), + 'volume_type': random.choice(['fake_lvmdriver-1', 'fake_lvmdriver-2']), + 'bootable': random.randint(0, 1), + 'metadata': { + 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, + 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, + 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, + }, + 'snapshot_id': random.randint(1, 5), + 'availability_zone': 'zone' + uuid.uuid4().hex, + 'attachments': [ + { + 'device': '/dev/' + uuid.uuid4().hex, + 'server_id': uuid.uuid4().hex, + }, + ], + } + + # Overwrite default attributes if there are some attributes set + volume_info.update(attrs) + + volume = fakes.FakeResource(None, volume_info, loaded=True) + return volume + + +def create_volumes(attrs=None, count=2): + """Create multiple fake volumes. + + :param dict attrs: + A dictionary with all attributes of volume + :param Integer count: + The number of volumes to be faked + :return: + A list of FakeResource objects + """ + volumes = [] + for n in range(0, count): + volumes.append(create_one_volume(attrs)) + + return volumes + + +def create_one_sdk_volume(attrs=None): + """Create a fake volume. + + :param dict attrs: + A dictionary with all attributes of volume + :return: + A FakeResource object with id, name, status, etc. + """ + attrs = attrs or {} + + # Set default attribute + volume_info = { + 'id': 'volume-id' + uuid.uuid4().hex, + 'name': 'volume-name' + uuid.uuid4().hex, + 'description': 'description' + uuid.uuid4().hex, + 'status': random.choice(['available', 'in_use']), + 'size': random.randint(1, 20), + 'volume_type': random.choice(['fake_lvmdriver-1', 'fake_lvmdriver-2']), + 'bootable': random.choice(['true', 'false']), + 'metadata': { + 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, + 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, + 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, + }, + 'snapshot_id': random.randint(1, 5), + 'availability_zone': 'zone' + uuid.uuid4().hex, + 'attachments': [ + { + 'device': '/dev/' + uuid.uuid4().hex, + 'server_id': uuid.uuid4().hex, + }, + ], + } + + # Overwrite default attributes if there are some attributes set + volume_info.update(attrs) + return volume.Volume(**volume_info) + + +def create_sdk_volumes(attrs=None, count=2): + """Create multiple fake volumes. + + :param dict attrs: + A dictionary with all attributes of volume + :param Integer count: + The number of volumes to be faked + :return: + A list of FakeResource objects + """ + volumes = [] + for n in range(0, count): + volumes.append(create_one_sdk_volume(attrs)) + + return volumes + + +def get_volumes(volumes=None, count=2): + """Get an iterable MagicMock object with a list of faked volumes. + + If volumes list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List volumes: + A list of FakeResource objects faking volumes + :param Integer count: + The number of volumes to be faked + :return + An iterable Mock object with side_effect set to a list of faked + volumes + """ + if volumes is None: + volumes = create_volumes(count) + + return mock.Mock(side_effect=volumes) + + +def get_volume_columns(volume=None): + """Get the volume columns from a faked volume object. + + :param volume: + A FakeResource objects faking volume + :return + A tuple which may include the following keys: + ('id', 'name', 'description', 'status', 'size', 'volume_type', + 'metadata', 'snapshot', 'availability_zone', 'attachments') + """ + if volume is not None: + return tuple(k for k in sorted(volume.keys())) + return tuple([]) + + +def get_volume_data(volume=None): + """Get the volume data from a faked volume object. + + :param volume: + A FakeResource objects faking volume + :return + A tuple which may include the following values: + ('ce26708d', 'fake_volume', 'fake description', 'available', + 20, 'fake_lvmdriver-1', "Alpha='a', Beta='b', Gamma='g'", + 1, 'nova', [{'device': '/dev/ice', 'server_id': '1233'}]) + """ + data_list = [] + if volume is not None: + for x in sorted(volume.keys()): + if x == 'tags': + # The 'tags' should be format_list + data_list.append(format_columns.ListColumn(volume.info.get(x))) + else: + data_list.append(volume.info.get(x)) + return tuple(data_list) + + +def create_one_availability_zone(attrs=None): + """Create a fake AZ. + + :param dict attrs: + A dictionary with all attributes + :return: + A FakeResource object with zoneName, zoneState, etc. + """ + attrs = attrs or {} + + # Set default attributes. + availability_zone = { + 'zoneName': uuid.uuid4().hex, + 'zoneState': {'available': True}, + } + + # Overwrite default attributes. + availability_zone.update(attrs) + + availability_zone = fakes.FakeResource( + info=copy.deepcopy(availability_zone), loaded=True + ) + return availability_zone + + +def create_availability_zones(attrs=None, count=2): + """Create multiple fake AZs. + + :param dict attrs: + A dictionary with all attributes + :param int count: + The number of AZs to fake + :return: + A list of FakeResource objects faking the AZs + """ + availability_zones = [] + for i in range(0, count): + availability_zone = create_one_availability_zone(attrs) + availability_zones.append(availability_zone) + + return availability_zones + + +def create_one_backup(attrs=None): + """Create a fake backup. + + :param dict attrs: + A dictionary with all attributes + :return: + A FakeResource object with id, name, volume_id, etc. + """ + attrs = attrs or {} + + # Set default attributes. + backup_info = { + "id": 'backup-id-' + uuid.uuid4().hex, + "name": 'backup-name-' + uuid.uuid4().hex, + "volume_id": 'volume-id-' + uuid.uuid4().hex, + "snapshot_id": 'snapshot-id' + uuid.uuid4().hex, + "description": 'description-' + uuid.uuid4().hex, + "object_count": None, + "container": 'container-' + uuid.uuid4().hex, + "size": random.randint(1, 20), + "status": "error", + "availability_zone": 'zone' + uuid.uuid4().hex, + } + + # Overwrite default attributes. + backup_info.update(attrs) + + backup = fakes.FakeResource(info=copy.deepcopy(backup_info), loaded=True) + return backup + + +def create_backups(attrs=None, count=2): + """Create multiple fake backups. + + :param dict attrs: + A dictionary with all attributes + :param int count: + The number of backups to fake + :return: + A list of FakeResource objects faking the backups + """ + backups = [] + for i in range(0, count): + backup = create_one_backup(attrs) + backups.append(backup) + + return backups + + +def get_backups(backups=None, count=2): + """Get an iterable MagicMock object with a list of faked backups. + + If backups list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List backups: + A list of FakeResource objects faking backups + :param Integer count: + The number of backups to be faked + :return + An iterable Mock object with side_effect set to a list of faked + backups + """ + if backups is None: + backups = create_backups(count) + + return mock.Mock(side_effect=backups) + + +def create_backup_record(): + """Gets a fake backup record for a given backup. + + :return: An "exported" backup record. + """ + + return { + 'backup_service': 'cinder.backup.drivers.swift.SwiftBackupDriver', + 'backup_url': 'eyJzdGF0dXMiOiAiYXZh', + } + + +def import_backup_record(): + """Creates a fake backup record import response from a backup. + + :return: The fake backup object that was encoded. + """ + return { + 'backup': { + 'id': 'backup.id', + 'name': 'backup.name', + 'links': [ + {'href': 'link1', 'rel': 'self'}, + {'href': 'link2', 'rel': 'bookmark'}, + ], + }, + } + + +def create_one_consistency_group(attrs=None): + """Create a fake consistency group. + + :param dict attrs: + A dictionary with all attributes + :return: + A FakeResource object with id, name, description, etc. + """ + attrs = attrs or {} + + # Set default attributes. + consistency_group_info = { + "id": 'backup-id-' + uuid.uuid4().hex, + "name": 'backup-name-' + uuid.uuid4().hex, + "description": 'description-' + uuid.uuid4().hex, + "status": "error", + "availability_zone": 'zone' + uuid.uuid4().hex, + "created_at": 'time-' + uuid.uuid4().hex, + "volume_types": ['volume-type1'], + } + + # Overwrite default attributes. + consistency_group_info.update(attrs) + + consistency_group = fakes.FakeResource( + info=copy.deepcopy(consistency_group_info), loaded=True + ) + return consistency_group + + +def create_consistency_groups(attrs=None, count=2): + """Create multiple fake consistency groups. + + :param dict attrs: + A dictionary with all attributes + :param int count: + The number of consistency groups to fake + :return: + A list of FakeResource objects faking the consistency groups + """ + consistency_groups = [] + for i in range(0, count): + consistency_group = create_one_consistency_group(attrs) + consistency_groups.append(consistency_group) + + return consistency_groups + + +def get_consistency_groups(consistency_groups=None, count=2): + """Note: + + Get an iterable MagicMock object with a list of faked + consistency_groups. + + If consistency_groups list is provided, then initialize + the Mock object with the list. Otherwise create one. + + :param List consistency_groups: + A list of FakeResource objects faking consistency_groups + :param Integer count: + The number of consistency_groups to be faked + :return + An iterable Mock object with side_effect set to a list of faked + consistency_groups + """ + if consistency_groups is None: + consistency_groups = create_consistency_groups(count) + + return mock.Mock(side_effect=consistency_groups) + + +def create_one_consistency_group_snapshot(attrs=None): + """Create a fake consistency group snapshot. + + :param dict attrs: + A dictionary with all attributes + :return: + A FakeResource object with id, name, description, etc. + """ + attrs = attrs or {} + + # Set default attributes. + consistency_group_snapshot_info = { + "id": 'id-' + uuid.uuid4().hex, + "name": 'backup-name-' + uuid.uuid4().hex, + "description": 'description-' + uuid.uuid4().hex, + "status": "error", + "consistencygroup_id": 'consistency-group-id' + uuid.uuid4().hex, + "created_at": 'time-' + uuid.uuid4().hex, + } + + # Overwrite default attributes. + consistency_group_snapshot_info.update(attrs) + + consistency_group_snapshot = fakes.FakeResource( + info=copy.deepcopy(consistency_group_snapshot_info), loaded=True + ) + return consistency_group_snapshot + + +def create_consistency_group_snapshots(attrs=None, count=2): + """Create multiple fake consistency group snapshots. + + :param dict attrs: + A dictionary with all attributes + :param int count: + The number of consistency group snapshots to fake + :return: + A list of FakeResource objects faking the + consistency group snapshots + """ + consistency_group_snapshots = [] + for i in range(0, count): + consistency_group_snapshot = create_one_consistency_group_snapshot( + attrs, + ) + consistency_group_snapshots.append(consistency_group_snapshot) + + return consistency_group_snapshots + + +def get_consistency_group_snapshots(snapshots=None, count=2): + """Get an iterable MagicMock object with a list of faked cgsnapshots. + + If consistenct group snapshots list is provided, then initialize + the Mock object with the list. Otherwise create one. + + :param List snapshots: + A list of FakeResource objects faking consistency group snapshots + :param Integer count: + The number of consistency group snapshots to be faked + :return + An iterable Mock object with side_effect set to a list of faked + consistency groups + """ + if snapshots is None: + snapshots = create_consistency_group_snapshots(count) + + return mock.Mock(side_effect=snapshots) + + +def create_one_extension(attrs=None): + """Create a fake extension. + + :param dict attrs: + A dictionary with all attributes + :return: + A FakeResource object with name, namespace, etc. + """ + attrs = attrs or {} + + # Set default attributes. + extension_info = { + 'name': 'name-' + uuid.uuid4().hex, + 'namespace': ( + 'http://docs.openstack.org/' + 'block-service/ext/scheduler-hints/api/v2' + ), + 'description': 'description-' + uuid.uuid4().hex, + 'updated': '2013-04-18T00:00:00+00:00', + 'alias': 'OS-SCH-HNT', + 'links': ( + '[{"href":' + '"https://github.com/openstack/block-api", "type":' + ' "text/html", "rel": "describedby"}]' + ), + } + + # Overwrite default attributes. + extension_info.update(attrs) + + extension = fakes.FakeResource( + info=copy.deepcopy(extension_info), loaded=True + ) + return extension + + +def create_one_qos(attrs=None): + """Create a fake Qos specification. + + :param dict attrs: + A dictionary with all attributes + :return: + A FakeResource object with id, name, consumer, etc. + """ + attrs = attrs or {} + + # Set default attributes. + qos_info = { + "id": 'qos-id-' + uuid.uuid4().hex, + "name": 'qos-name-' + uuid.uuid4().hex, + "consumer": 'front-end', + "specs": {"foo": "bar", "iops": "9001"}, + } + + # Overwrite default attributes. + qos_info.update(attrs) + + qos = fakes.FakeResource(info=copy.deepcopy(qos_info), loaded=True) + return qos + + +def create_one_qos_association(attrs=None): + """Create a fake Qos specification association. + + :param dict attrs: + A dictionary with all attributes + :return: + A FakeResource object with id, name, association_type, etc. + """ + attrs = attrs or {} + + # Set default attributes. + qos_association_info = { + "id": 'type-id-' + uuid.uuid4().hex, + "name": 'type-name-' + uuid.uuid4().hex, + "association_type": 'volume_type', + } + + # Overwrite default attributes. + qos_association_info.update(attrs) + + qos_association = fakes.FakeResource( + info=copy.deepcopy(qos_association_info), loaded=True + ) + return qos_association + + +def create_qoses(attrs=None, count=2): + """Create multiple fake Qos specifications. + + :param dict attrs: + A dictionary with all attributes + :param int count: + The number of Qos specifications to fake + :return: + A list of FakeResource objects faking the Qos specifications + """ + qoses = [] + for i in range(0, count): + qos = create_one_qos(attrs) + qoses.append(qos) + + return qoses + + +def get_qoses(qoses=None, count=2): + """Get an iterable MagicMock object with a list of faked qoses. + + If qoses list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List qoses: + A list of FakeResource objects faking qoses + :param Integer count: + The number of qoses to be faked + :return + An iterable Mock object with side_effect set to a list of faked + qoses + """ + if qoses is None: + qoses = create_qoses(count) + + return mock.Mock(side_effect=qoses) + + +def create_one_snapshot(attrs=None): + """Create a fake snapshot. + + :param dict attrs: + A dictionary with all attributes + :return: + A FakeResource object with id, name, description, etc. + """ + attrs = attrs or {} + + # Set default attributes. + snapshot_info = { + "id": 'snapshot-id-' + uuid.uuid4().hex, + "name": 'snapshot-name-' + uuid.uuid4().hex, + "description": 'snapshot-description-' + uuid.uuid4().hex, + "size": 10, + "status": "available", + "metadata": {"foo": "bar"}, + "created_at": "2015-06-03T18:49:19.000000", + "volume_id": 'vloume-id-' + uuid.uuid4().hex, + } + + # Overwrite default attributes. + snapshot_info.update(attrs) + + snapshot = fakes.FakeResource( + info=copy.deepcopy(snapshot_info), loaded=True + ) + return snapshot + + +def create_snapshots(attrs=None, count=2): + """Create multiple fake snapshots. + + :param dict attrs: + A dictionary with all attributes + :param int count: + The number of snapshots to fake + :return: + A list of FakeResource objects faking the snapshots + """ + snapshots = [] + for i in range(0, count): + snapshot = create_one_snapshot(attrs) + snapshots.append(snapshot) + + return snapshots + + +def get_snapshots(snapshots=None, count=2): + """Get an iterable MagicMock object with a list of faked snapshots. + + If snapshots list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List snapshots: + A list of FakeResource objects faking snapshots + :param Integer count: + The number of snapshots to be faked + :return + An iterable Mock object with side_effect set to a list of faked + snapshots + """ + if snapshots is None: + snapshots = create_snapshots(count) + + return mock.Mock(side_effect=snapshots) + + +def create_one_volume_type(attrs=None, methods=None): + """Create a fake volume type. + + :param dict attrs: + A dictionary with all attributes + :param dict methods: + A dictionary with all methods + :return: + A FakeResource object with id, name, description, etc. + """ + attrs = attrs or {} + methods = methods or {} + + # Set default attributes. + volume_type_info = { + "id": 'type-id-' + uuid.uuid4().hex, + "name": 'type-name-' + uuid.uuid4().hex, + "description": 'type-description-' + uuid.uuid4().hex, + "extra_specs": {"foo": "bar"}, + "is_public": True, + } + + # Overwrite default attributes. + volume_type_info.update(attrs) + + volume_type = fakes.FakeResource( + info=copy.deepcopy(volume_type_info), methods=methods, loaded=True + ) + return volume_type + + +def create_volume_types(attrs=None, count=2): + """Create multiple fake volume_types. + + :param dict attrs: + A dictionary with all attributes + :param int count: + The number of types to fake + :return: + A list of FakeResource objects faking the types + """ + volume_types = [] + for i in range(0, count): + volume_type = create_one_volume_type(attrs) + volume_types.append(volume_type) + + return volume_types + + +def get_volume_types(volume_types=None, count=2): + """Get an iterable MagicMock object with a list of faked volume types. + + If volume_types list is provided, then initialize the Mock object with + the list. Otherwise create one. + + :param List volume_types: + A list of FakeResource objects faking volume types + :param Integer count: + The number of volume types to be faked + :return + An iterable Mock object with side_effect set to a list of faked + volume types + """ + if volume_types is None: + volume_types = create_volume_types(count) + + return mock.Mock(side_effect=volume_types) + + +def create_one_encryption_volume_type(attrs=None): + """Create a fake encryption volume type. + + :param dict attrs: + A dictionary with all attributes + :return: + A FakeResource object with volume_type_id etc. + """ + attrs = attrs or {} + + # Set default attributes. + encryption_info = { + "volume_type_id": 'type-id-' + uuid.uuid4().hex, + 'provider': 'LuksEncryptor', + 'cipher': None, + 'key_size': None, + 'control_location': 'front-end', + } + + # Overwrite default attributes. + encryption_info.update(attrs) + + encryption_type = fakes.FakeResource( + info=copy.deepcopy(encryption_info), loaded=True + ) + return encryption_type + + +def create_one_vol_quota(attrs=None): + """Create one quota""" + attrs = attrs or {} + + quota_attrs = { + 'id': 'project-id-' + uuid.uuid4().hex, + 'backups': 100, + 'backup_gigabytes': 100, + 'gigabytes': 10, + 'per_volume_gigabytes': 10, + 'snapshots': 0, + 'volumes': 10, + } + + quota_attrs.update(attrs) + + quota = fakes.FakeResource(info=copy.deepcopy(quota_attrs), loaded=True) + quota.project_id = quota_attrs['id'] + + return quota + + +def create_one_default_vol_quota(attrs=None): + """Create one quota""" + attrs = attrs or {} + + quota_attrs = { + 'id': 'project-id-' + uuid.uuid4().hex, + 'backups': 100, + 'backup_gigabytes': 100, + 'gigabytes': 100, + 'per_volume_gigabytes': 100, + 'snapshots': 100, + 'volumes': 100, + } + + quota_attrs.update(attrs) + + quota = fakes.FakeResource(info=copy.deepcopy(quota_attrs), loaded=True) + quota.project_id = quota_attrs['id'] + + return quota + + +def create_one_detailed_quota(attrs=None): + """Create one quota""" + attrs = attrs or {} -class FakeQos(object): - """Fake one or more Qos specification.""" - - @staticmethod - def create_one_qos(attrs=None): - """Create a fake Qos specification. - - :param dict attrs: - A dictionary with all attributes - :return: - A FakeResource object with id, name, consumer, etc. - """ - attrs = attrs or {} - - # Set default attributes. - qos_info = { - "id": 'qos-id-' + uuid.uuid4().hex, - "name": 'qos-name-' + uuid.uuid4().hex, - "consumer": 'front-end', - "specs": {"foo": "bar", "iops": "9001"}, - } - - # Overwrite default attributes. - qos_info.update(attrs) - - qos = fakes.FakeResource( - info=copy.deepcopy(qos_info), - loaded=True) - return qos - - @staticmethod - def create_one_qos_association(attrs=None): - """Create a fake Qos specification association. - - :param dict attrs: - A dictionary with all attributes - :return: - A FakeResource object with id, name, association_type, etc. - """ - attrs = attrs or {} - - # Set default attributes. - qos_association_info = { - "id": 'type-id-' + uuid.uuid4().hex, - "name": 'type-name-' + uuid.uuid4().hex, - "association_type": 'volume_type', - } - - # Overwrite default attributes. - qos_association_info.update(attrs) - - qos_association = fakes.FakeResource( - info=copy.deepcopy(qos_association_info), - loaded=True) - return qos_association - - @staticmethod - def create_qoses(attrs=None, count=2): - """Create multiple fake Qos specifications. - - :param dict attrs: - A dictionary with all attributes - :param int count: - The number of Qos specifications to fake - :return: - A list of FakeResource objects faking the Qos specifications - """ - qoses = [] - for i in range(0, count): - qos = FakeQos.create_one_qos(attrs) - qoses.append(qos) - - return qoses - - @staticmethod - def get_qoses(qoses=None, count=2): - """Get an iterable MagicMock object with a list of faked qoses. - - If qoses list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param List qoses: - A list of FakeResource objects faking qoses - :param Integer count: - The number of qoses to be faked - :return - An iterable Mock object with side_effect set to a list of faked - qoses - """ - if qoses is None: - qoses = FakeQos.create_qoses(count) - - return mock.Mock(side_effect=qoses) - - -class FakeSnapshot(object): - """Fake one or more snapshot.""" - - @staticmethod - def create_one_snapshot(attrs=None): - """Create a fake snapshot. - - :param dict attrs: - A dictionary with all attributes - :return: - A FakeResource object with id, name, description, etc. - """ - attrs = attrs or {} - - # Set default attributes. - snapshot_info = { - "id": 'snapshot-id-' + uuid.uuid4().hex, - "name": 'snapshot-name-' + uuid.uuid4().hex, - "description": 'snapshot-description-' + uuid.uuid4().hex, - "size": 10, - "status": "available", - "metadata": {"foo": "bar"}, - "created_at": "2015-06-03T18:49:19.000000", - "volume_id": 'vloume-id-' + uuid.uuid4().hex, - } - - # Overwrite default attributes. - snapshot_info.update(attrs) - - snapshot = fakes.FakeResource( - info=copy.deepcopy(snapshot_info), - loaded=True) - return snapshot - - @staticmethod - def create_snapshots(attrs=None, count=2): - """Create multiple fake snapshots. - - :param dict attrs: - A dictionary with all attributes - :param int count: - The number of snapshots to fake - :return: - A list of FakeResource objects faking the snapshots - """ - snapshots = [] - for i in range(0, count): - snapshot = FakeSnapshot.create_one_snapshot(attrs) - snapshots.append(snapshot) - - return snapshots - - @staticmethod - def get_snapshots(snapshots=None, count=2): - """Get an iterable MagicMock object with a list of faked snapshots. - - If snapshots list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param List snapshots: - A list of FakeResource objects faking snapshots - :param Integer count: - The number of snapshots to be faked - :return - An iterable Mock object with side_effect set to a list of faked - snapshots - """ - if snapshots is None: - snapshots = FakeSnapshot.create_snapshots(count) - - return mock.Mock(side_effect=snapshots) - - -class FakeVolumeType(object): - """Fake one or more volume type.""" - - @staticmethod - def create_one_volume_type(attrs=None, methods=None): - """Create a fake volume type. - - :param dict attrs: - A dictionary with all attributes - :param dict methods: - A dictionary with all methods - :return: - A FakeResource object with id, name, description, etc. - """ - attrs = attrs or {} - methods = methods or {} - - # Set default attributes. - volume_type_info = { - "id": 'type-id-' + uuid.uuid4().hex, - "name": 'type-name-' + uuid.uuid4().hex, - "description": 'type-description-' + uuid.uuid4().hex, - "extra_specs": {"foo": "bar"}, - "is_public": True, - } - - # Overwrite default attributes. - volume_type_info.update(attrs) - - volume_type = fakes.FakeResource( - info=copy.deepcopy(volume_type_info), - methods=methods, - loaded=True) - return volume_type - - @staticmethod - def create_volume_types(attrs=None, count=2): - """Create multiple fake volume_types. - - :param dict attrs: - A dictionary with all attributes - :param int count: - The number of types to fake - :return: - A list of FakeResource objects faking the types - """ - volume_types = [] - for i in range(0, count): - volume_type = FakeVolumeType.create_one_volume_type(attrs) - volume_types.append(volume_type) - - return volume_types - - @staticmethod - def get_volume_types(volume_types=None, count=2): - """Get an iterable MagicMock object with a list of faked volume types. - - If volume_types list is provided, then initialize the Mock object with - the list. Otherwise create one. - - :param List volume_types: - A list of FakeResource objects faking volume types - :param Integer count: - The number of volume types to be faked - :return - An iterable Mock object with side_effect set to a list of faked - volume types - """ - if volume_types is None: - volume_types = FakeVolumeType.create_volume_types(count) - - return mock.Mock(side_effect=volume_types) - - @staticmethod - def create_one_encryption_volume_type(attrs=None): - """Create a fake encryption volume type. - - :param dict attrs: - A dictionary with all attributes - :return: - A FakeResource object with volume_type_id etc. - """ - attrs = attrs or {} - - # Set default attributes. - encryption_info = { - "volume_type_id": 'type-id-' + uuid.uuid4().hex, - 'provider': 'LuksEncryptor', - 'cipher': None, - 'key_size': None, - 'control_location': 'front-end', - } - - # Overwrite default attributes. - encryption_info.update(attrs) - - encryption_type = fakes.FakeResource( - info=copy.deepcopy(encryption_info), - loaded=True) - return encryption_type - - -class FakeQuota(object): - """Fake quota""" - - @staticmethod - def create_one_vol_quota(attrs=None): - """Create one quota""" - attrs = attrs or {} - - quota_attrs = { - 'id': 'project-id-' + uuid.uuid4().hex, - 'backups': 100, - 'backup_gigabytes': 100, - 'gigabytes': 10, - 'per_volume_gigabytes': 10, - 'snapshots': 0, - 'volumes': 10} - - quota_attrs.update(attrs) - - quota = fakes.FakeResource( - info=copy.deepcopy(quota_attrs), - loaded=True) - quota.project_id = quota_attrs['id'] - - return quota - - @staticmethod - def create_one_default_vol_quota(attrs=None): - """Create one quota""" - attrs = attrs or {} - - quota_attrs = { - 'id': 'project-id-' + uuid.uuid4().hex, - 'backups': 100, - 'backup_gigabytes': 100, - 'gigabytes': 100, - 'per_volume_gigabytes': 100, - 'snapshots': 100, - 'volumes': 100} - - quota_attrs.update(attrs) - - quota = fakes.FakeResource( - info=copy.deepcopy(quota_attrs), - loaded=True) - quota.project_id = quota_attrs['id'] - - return quota - - @staticmethod - def create_one_detailed_quota(attrs=None): - """Create one quota""" - attrs = attrs or {} - - quota_attrs = { - 'volumes': {'limit': 3, 'in_use': 1, 'reserved': 0}, - 'per_volume_gigabytes': {'limit': -1, 'in_use': 0, 'reserved': 0}, - 'snapshots': {'limit': 10, 'in_use': 0, 'reserved': 0}, - 'gigabytes': {'limit': 1000, 'in_use': 5, 'reserved': 0}, - 'backups': {'limit': 10, 'in_use': 0, 'reserved': 0}, - 'backup_gigabytes': {'limit': 1000, 'in_use': 0, 'reserved': 0}, - 'volumes_lvmdriver-1': {'limit': -1, 'in_use': 1, 'reserved': 0}, - 'gigabytes_lvmdriver-1': {'limit': -1, 'in_use': 5, 'reserved': 0}, - 'snapshots_lvmdriver-1': {'limit': -1, 'in_use': 0, 'reserved': 0}, - 'volumes___DEFAULT__': {'limit': -1, 'in_use': 0, 'reserved': 0}, - 'gigabytes___DEFAULT__': {'limit': -1, 'in_use': 0, 'reserved': 0}, - 'snapshots___DEFAULT__': {'limit': -1, 'in_use': 0, 'reserved': 0}, - 'groups': {'limit': 10, 'in_use': 0, 'reserved': 0}, - 'id': uuid.uuid4().hex, - } - quota_attrs.update(attrs) + quota_attrs = { + 'volumes': {'limit': 3, 'in_use': 1, 'reserved': 0}, + 'per_volume_gigabytes': {'limit': -1, 'in_use': 0, 'reserved': 0}, + 'snapshots': {'limit': 10, 'in_use': 0, 'reserved': 0}, + 'gigabytes': {'limit': 1000, 'in_use': 5, 'reserved': 0}, + 'backups': {'limit': 10, 'in_use': 0, 'reserved': 0}, + 'backup_gigabytes': {'limit': 1000, 'in_use': 0, 'reserved': 0}, + 'volumes_lvmdriver-1': {'limit': -1, 'in_use': 1, 'reserved': 0}, + 'gigabytes_lvmdriver-1': {'limit': -1, 'in_use': 5, 'reserved': 0}, + 'snapshots_lvmdriver-1': {'limit': -1, 'in_use': 0, 'reserved': 0}, + 'volumes___DEFAULT__': {'limit': -1, 'in_use': 0, 'reserved': 0}, + 'gigabytes___DEFAULT__': {'limit': -1, 'in_use': 0, 'reserved': 0}, + 'snapshots___DEFAULT__': {'limit': -1, 'in_use': 0, 'reserved': 0}, + 'groups': {'limit': 10, 'in_use': 0, 'reserved': 0}, + 'id': uuid.uuid4().hex, + } + quota_attrs.update(attrs) - quota = fakes.FakeResource( - info=copy.deepcopy(quota_attrs), - loaded=True) + quota = fakes.FakeResource(info=copy.deepcopy(quota_attrs), loaded=True) - return quota + return quota class FakeLimits(object): @@ -1237,37 +1146,39 @@ def __init__(self, absolute_attrs=None): 'maxTotalVolumes': 10, 'totalVolumesUsed': 4, 'totalBackupsUsed': 0, - 'totalGigabytesUsed': 35 + 'totalGigabytesUsed': 35, } absolute_attrs = absolute_attrs or {} self.absolute_limits_attrs.update(absolute_attrs) - self.rate_limits_attrs = [{ - "uri": "*", - "limit": [ - { - "value": 10, - "verb": "POST", - "remaining": 2, - "unit": "MINUTE", - "next-available": "2011-12-15T22:42:45Z" - }, - { - "value": 10, - "verb": "PUT", - "remaining": 2, - "unit": "MINUTE", - "next-available": "2011-12-15T22:42:45Z" - }, - { - "value": 100, - "verb": "DELETE", - "remaining": 100, - "unit": "MINUTE", - "next-available": "2011-12-15T22:42:45Z" - } - ] - }] + self.rate_limits_attrs = [ + { + "uri": "*", + "limit": [ + { + "value": 10, + "verb": "POST", + "remaining": 2, + "unit": "MINUTE", + "next-available": "2011-12-15T22:42:45Z", + }, + { + "value": 10, + "verb": "PUT", + "remaining": 2, + "unit": "MINUTE", + "next-available": "2011-12-15T22:42:45Z", + }, + { + "value": 100, + "verb": "DELETE", + "remaining": 100, + "unit": "MINUTE", + "next-available": "2011-12-15T22:42:45Z", + }, + ], + } + ] @property def absolute(self): @@ -1285,18 +1196,30 @@ def rate(self): for group in self.rate_limits_attrs: uri = group['uri'] for rate in group['limit']: - yield FakeRateLimit(rate['verb'], uri, rate['value'], - rate['remaining'], rate['unit'], - rate['next-available']) + yield FakeRateLimit( + rate['verb'], + uri, + rate['value'], + rate['remaining'], + rate['unit'], + rate['next-available'], + ) def rate_limits(self): reference_data = [] for group in self.rate_limits_attrs: uri = group['uri'] for rate in group['limit']: - reference_data.append((rate['verb'], uri, rate['value'], - rate['remaining'], rate['unit'], - rate['next-available'])) + reference_data.append( + ( + rate['verb'], + uri, + rate['value'], + rate['remaining'], + rate['unit'], + rate['next-available'], + ) + ) return reference_data @@ -1311,8 +1234,7 @@ def __init__(self, name, value): class FakeRateLimit(object): """Data model that represents a flattened view of a single rate limit.""" - def __init__(self, verb, uri, value, remain, - unit, next_available): + def __init__(self, verb, uri, value, remain, unit, next_available): self.verb = verb self.uri = uri self.value = value diff --git a/openstackclient/tests/unit/volume/v2/test_backup_record.py b/openstackclient/tests/unit/volume/v2/test_backup_record.py index 0e24174c5e..aa9a25a223 100644 --- a/openstackclient/tests/unit/volume/v2/test_backup_record.py +++ b/openstackclient/tests/unit/volume/v2/test_backup_record.py @@ -19,7 +19,7 @@ class TestBackupRecord(volume_fakes.TestVolume): def setUp(self): - super(TestBackupRecord, self).setUp() + super().setUp() self.backups_mock = self.app.client_manager.volume.backups self.backups_mock.reset_mock() @@ -27,12 +27,13 @@ def setUp(self): class TestBackupRecordExport(TestBackupRecord): - new_backup = volume_fakes.FakeBackup.create_one_backup( - attrs={'volume_id': 'a54708a2-0388-4476-a909-09579f885c25'}) - new_record = volume_fakes.FakeBackup.create_backup_record() + new_backup = volume_fakes.create_one_backup( + attrs={'volume_id': 'a54708a2-0388-4476-a909-09579f885c25'}, + ) + new_record = volume_fakes.create_backup_record() def setUp(self): - super(TestBackupRecordExport, self).setUp() + super().setUp() self.backups_mock.export_record.return_value = self.new_record self.backups_mock.get.return_value = self.new_backup @@ -81,12 +82,13 @@ def test_backup_export_json(self): class TestBackupRecordImport(TestBackupRecord): - new_backup = volume_fakes.FakeBackup.create_one_backup( - attrs={'volume_id': 'a54708a2-0388-4476-a909-09579f885c25'}) - new_import = volume_fakes.FakeBackup.import_backup_record() + new_backup = volume_fakes.create_one_backup( + attrs={'volume_id': 'a54708a2-0388-4476-a909-09579f885c25'}, + ) + new_import = volume_fakes.import_backup_record() def setUp(self): - super(TestBackupRecordImport, self).setUp() + super().setUp() self.backups_mock.import_record.return_value = self.new_import diff --git a/openstackclient/tests/unit/volume/v2/test_consistency_group.py b/openstackclient/tests/unit/volume/v2/test_consistency_group.py index 434d0cf91a..7ef4a08ea7 100644 --- a/openstackclient/tests/unit/volume/v2/test_consistency_group.py +++ b/openstackclient/tests/unit/volume/v2/test_consistency_group.py @@ -26,7 +26,7 @@ class TestConsistencyGroup(volume_fakes.TestVolume): def setUp(self): - super(TestConsistencyGroup, self).setUp() + super().setUp() # Get a shortcut to the TransferManager Mock self.consistencygroups_mock = ( @@ -47,11 +47,10 @@ def setUp(self): class TestConsistencyGroupAddVolume(TestConsistencyGroup): - _consistency_group = ( - volume_fakes.FakeConsistencyGroup.create_one_consistency_group()) + _consistency_group = volume_fakes.create_one_consistency_group() def setUp(self): - super(TestConsistencyGroupAddVolume, self).setUp() + super().setUp() self.consistencygroups_mock.get.return_value = ( self._consistency_group) @@ -60,7 +59,7 @@ def setUp(self): consistency_group.AddVolumeToConsistencyGroup(self.app, None) def test_add_one_volume_to_consistency_group(self): - volume = volume_fakes.FakeVolume.create_one_volume() + volume = volume_fakes.create_one_volume() self.volumes_mock.get.return_value = volume arglist = [ self._consistency_group.id, @@ -85,8 +84,8 @@ def test_add_one_volume_to_consistency_group(self): self.assertIsNone(result) def test_add_multiple_volumes_to_consistency_group(self): - volumes = volume_fakes.FakeVolume.create_volumes(count=2) - self.volumes_mock.get = volume_fakes.FakeVolume.get_volumes(volumes) + volumes = volume_fakes.create_volumes(count=2) + self.volumes_mock.get = volume_fakes.get_volumes(volumes) arglist = [ self._consistency_group.id, volumes[0].id, @@ -112,8 +111,9 @@ def test_add_multiple_volumes_to_consistency_group(self): @mock.patch.object(consistency_group.LOG, 'error') def test_add_multiple_volumes_to_consistency_group_with_exception( - self, mock_error): - volume = volume_fakes.FakeVolume.create_one_volume() + self, mock_error, + ): + volume = volume_fakes.create_one_volume() arglist = [ self._consistency_group.id, volume.id, @@ -148,13 +148,10 @@ def test_add_multiple_volumes_to_consistency_group_with_exception( class TestConsistencyGroupCreate(TestConsistencyGroup): - volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() - new_consistency_group = ( - volume_fakes.FakeConsistencyGroup.create_one_consistency_group()) + volume_type = volume_fakes.create_one_volume_type() + new_consistency_group = volume_fakes.create_one_consistency_group() consistency_group_snapshot = ( - volume_fakes. - FakeConsistencyGroupSnapshot. - create_one_consistency_group_snapshot() + volume_fakes.create_one_consistency_group_snapshot() ) columns = ( @@ -177,7 +174,7 @@ class TestConsistencyGroupCreate(TestConsistencyGroup): ) def setUp(self): - super(TestConsistencyGroupCreate, self).setUp() + super().setUp() self.consistencygroups_mock.create.return_value = ( self.new_consistency_group) self.consistencygroups_mock.create_from_src.return_value = ( @@ -313,13 +310,14 @@ def test_consistency_group_create_from_snapshot(self): class TestConsistencyGroupDelete(TestConsistencyGroup): consistency_groups =\ - volume_fakes.FakeConsistencyGroup.create_consistency_groups(count=2) + volume_fakes.create_consistency_groups(count=2) def setUp(self): - super(TestConsistencyGroupDelete, self).setUp() + super().setUp() - self.consistencygroups_mock.get = volume_fakes.FakeConsistencyGroup.\ - get_consistency_groups(self.consistency_groups) + self.consistencygroups_mock.get = volume_fakes.get_consistency_groups( + self.consistency_groups, + ) self.consistencygroups_mock.delete.return_value = None # Get the command object to mock @@ -409,8 +407,7 @@ def test_delete_multiple_consistency_groups_with_exception(self): class TestConsistencyGroupList(TestConsistencyGroup): - consistency_groups = ( - volume_fakes.FakeConsistencyGroup.create_consistency_groups(count=2)) + consistency_groups = volume_fakes.create_consistency_groups(count=2) columns = [ 'ID', @@ -444,7 +441,7 @@ class TestConsistencyGroupList(TestConsistencyGroup): )) def setUp(self): - super(TestConsistencyGroupList, self).setUp() + super().setUp() self.consistencygroups_mock.list.return_value = self.consistency_groups # Get the command to test @@ -502,11 +499,10 @@ def test_consistency_group_list_with_long(self): class TestConsistencyGroupRemoveVolume(TestConsistencyGroup): - _consistency_group = ( - volume_fakes.FakeConsistencyGroup.create_one_consistency_group()) + _consistency_group = volume_fakes.create_one_consistency_group() def setUp(self): - super(TestConsistencyGroupRemoveVolume, self).setUp() + super().setUp() self.consistencygroups_mock.get.return_value = ( self._consistency_group) @@ -515,7 +511,7 @@ def setUp(self): consistency_group.RemoveVolumeFromConsistencyGroup(self.app, None) def test_remove_one_volume_from_consistency_group(self): - volume = volume_fakes.FakeVolume.create_one_volume() + volume = volume_fakes.create_one_volume() self.volumes_mock.get.return_value = volume arglist = [ self._consistency_group.id, @@ -540,8 +536,8 @@ def test_remove_one_volume_from_consistency_group(self): self.assertIsNone(result) def test_remove_multi_volumes_from_consistency_group(self): - volumes = volume_fakes.FakeVolume.create_volumes(count=2) - self.volumes_mock.get = volume_fakes.FakeVolume.get_volumes(volumes) + volumes = volume_fakes.create_volumes(count=2) + self.volumes_mock.get = volume_fakes.get_volumes(volumes) arglist = [ self._consistency_group.id, volumes[0].id, @@ -567,8 +563,10 @@ def test_remove_multi_volumes_from_consistency_group(self): @mock.patch.object(consistency_group.LOG, 'error') def test_remove_multiple_volumes_from_consistency_group_with_exception( - self, mock_error): - volume = volume_fakes.FakeVolume.create_one_volume() + self, + mock_error, + ): + volume = volume_fakes.create_one_volume() arglist = [ self._consistency_group.id, volume.id, @@ -603,11 +601,10 @@ def test_remove_multiple_volumes_from_consistency_group_with_exception( class TestConsistencyGroupSet(TestConsistencyGroup): - consistency_group = ( - volume_fakes.FakeConsistencyGroup.create_one_consistency_group()) + consistency_group = volume_fakes.create_one_consistency_group() def setUp(self): - super(TestConsistencyGroupSet, self).setUp() + super().setUp() self.consistencygroups_mock.get.return_value = ( self.consistency_group) @@ -677,10 +674,9 @@ class TestConsistencyGroupShow(TestConsistencyGroup): ) def setUp(self): - super(TestConsistencyGroupShow, self).setUp() + super().setUp() - self.consistency_group = ( - volume_fakes.FakeConsistencyGroup.create_one_consistency_group()) + self.consistency_group = volume_fakes.create_one_consistency_group() self.data = ( self.consistency_group.availability_zone, self.consistency_group.created_at, diff --git a/openstackclient/tests/unit/volume/v2/test_consistency_group_snapshot.py b/openstackclient/tests/unit/volume/v2/test_consistency_group_snapshot.py index 2202b85b98..e3c738c855 100644 --- a/openstackclient/tests/unit/volume/v2/test_consistency_group_snapshot.py +++ b/openstackclient/tests/unit/volume/v2/test_consistency_group_snapshot.py @@ -35,12 +35,9 @@ def setUp(self): class TestConsistencyGroupSnapshotCreate(TestConsistencyGroupSnapshot): _consistency_group_snapshot = ( - volume_fakes. - FakeConsistencyGroupSnapshot. - create_one_consistency_group_snapshot() + volume_fakes.create_one_consistency_group_snapshot() ) - consistency_group = ( - volume_fakes.FakeConsistencyGroup.create_one_consistency_group()) + consistency_group = volume_fakes.create_one_consistency_group() columns = ( 'consistencygroup_id', @@ -124,16 +121,16 @@ def test_consistency_group_snapshot_create_no_consistency_group(self): class TestConsistencyGroupSnapshotDelete(TestConsistencyGroupSnapshot): consistency_group_snapshots = ( - volume_fakes.FakeConsistencyGroupSnapshot. - create_consistency_group_snapshots(count=2) + volume_fakes.create_consistency_group_snapshots(count=2) ) def setUp(self): super(TestConsistencyGroupSnapshotDelete, self).setUp() self.cgsnapshots_mock.get = ( - volume_fakes.FakeConsistencyGroupSnapshot. - get_consistency_group_snapshots(self.consistency_group_snapshots) + volume_fakes.get_consistency_group_snapshots( + self.consistency_group_snapshots + ) ) self.cgsnapshots_mock.delete.return_value = None @@ -178,12 +175,9 @@ def test_multiple_consistency_group_snapshots_delete(self): class TestConsistencyGroupSnapshotList(TestConsistencyGroupSnapshot): consistency_group_snapshots = ( - volume_fakes.FakeConsistencyGroupSnapshot. - create_consistency_group_snapshots(count=2) - ) - consistency_group = ( - volume_fakes.FakeConsistencyGroup.create_one_consistency_group() + volume_fakes.create_consistency_group_snapshots(count=2) ) + consistency_group = volume_fakes.create_one_consistency_group() columns = [ 'ID', @@ -306,9 +300,7 @@ def test_consistency_group_snapshot_list_with_options(self): class TestConsistencyGroupSnapshotShow(TestConsistencyGroupSnapshot): _consistency_group_snapshot = ( - volume_fakes. - FakeConsistencyGroupSnapshot. - create_one_consistency_group_snapshot() + volume_fakes.create_one_consistency_group_snapshot() ) columns = ( diff --git a/openstackclient/tests/unit/volume/v2/test_qos_specs.py b/openstackclient/tests/unit/volume/v2/test_qos_specs.py index 41c1801407..6f258dd54c 100644 --- a/openstackclient/tests/unit/volume/v2/test_qos_specs.py +++ b/openstackclient/tests/unit/volume/v2/test_qos_specs.py @@ -39,8 +39,8 @@ def setUp(self): class TestQosAssociate(TestQos): - volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() - qos_spec = volume_fakes.FakeQos.create_one_qos() + volume_type = volume_fakes.create_one_volume_type() + qos_spec = volume_fakes.create_one_qos() def setUp(self): super(TestQosAssociate, self).setUp() @@ -82,7 +82,7 @@ class TestQosCreate(TestQos): def setUp(self): super(TestQosCreate, self).setUp() - self.new_qos_spec = volume_fakes.FakeQos.create_one_qos() + self.new_qos_spec = volume_fakes.create_one_qos() self.qos_mock.create.return_value = self.new_qos_spec self.data = ( @@ -164,13 +164,13 @@ def test_qos_create_with_properties(self): class TestQosDelete(TestQos): - qos_specs = volume_fakes.FakeQos.create_qoses(count=2) + qos_specs = volume_fakes.create_qoses(count=2) def setUp(self): super(TestQosDelete, self).setUp() self.qos_mock.get = ( - volume_fakes.FakeQos.get_qoses(self.qos_specs)) + volume_fakes.get_qoses(self.qos_specs)) # Get the command object to test self.cmd = qos_specs.DeleteQos(self.app, None) @@ -255,8 +255,8 @@ def test_delete_multiple_qoses_with_exception(self): class TestQosDisassociate(TestQos): - volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() - qos_spec = volume_fakes.FakeQos.create_one_qos() + volume_type = volume_fakes.create_one_volume_type() + qos_spec = volume_fakes.create_one_qos() def setUp(self): super(TestQosDisassociate, self).setUp() @@ -303,8 +303,8 @@ def test_qos_disassociate_with_all_volume_types(self): class TestQosList(TestQos): - qos_specs = volume_fakes.FakeQos.create_qoses(count=2) - qos_association = volume_fakes.FakeQos.create_one_qos_association() + qos_specs = volume_fakes.create_qoses(count=2) + qos_association = volume_fakes.create_one_qos_association() columns = ( 'ID', @@ -374,7 +374,7 @@ def test_qos_list_no_association(self): class TestQosSet(TestQos): - qos_spec = volume_fakes.FakeQos.create_one_qos() + qos_spec = volume_fakes.create_one_qos() def setUp(self): super(TestQosSet, self).setUp() @@ -406,8 +406,8 @@ def test_qos_set_with_properties_with_id(self): class TestQosShow(TestQos): - qos_spec = volume_fakes.FakeQos.create_one_qos() - qos_association = volume_fakes.FakeQos.create_one_qos_association() + qos_spec = volume_fakes.create_one_qos() + qos_association = volume_fakes.create_one_qos_association() columns = ( 'associations', @@ -454,7 +454,7 @@ def test_qos_show(self): class TestQosUnset(TestQos): - qos_spec = volume_fakes.FakeQos.create_one_qos() + qos_spec = volume_fakes.create_one_qos() def setUp(self): super(TestQosUnset, self).setUp() diff --git a/openstackclient/tests/unit/volume/v2/test_service.py b/openstackclient/tests/unit/volume/v2/test_service.py index 3e9b2df91d..e9e39f4181 100644 --- a/openstackclient/tests/unit/volume/v2/test_service.py +++ b/openstackclient/tests/unit/volume/v2/test_service.py @@ -14,14 +14,14 @@ from osc_lib import exceptions -from openstackclient.tests.unit.volume.v2 import fakes as service_fakes +from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import service -class TestService(service_fakes.TestVolume): +class TestService(volume_fakes.TestVolume): def setUp(self): - super(TestService, self).setUp() + super().setUp() # Get a shortcut to the ServiceManager Mock self.service_mock = self.app.client_manager.volume.services @@ -31,10 +31,10 @@ def setUp(self): class TestServiceList(TestService): # The service to be listed - services = service_fakes.FakeService.create_one_service() + services = volume_fakes.create_one_service() def setUp(self): - super(TestServiceList, self).setUp() + super().setUp() self.service_mock.list.return_value = [self.services] @@ -144,10 +144,10 @@ def test_service_list_with_long_option(self): class TestServiceSet(TestService): - service = service_fakes.FakeService.create_one_service() + service = volume_fakes.create_one_service() def setUp(self): - super(TestServiceSet, self).setUp() + super().setUp() self.service_mock.enable.return_value = self.service self.service_mock.disable.return_value = self.service diff --git a/openstackclient/tests/unit/volume/v2/test_type.py b/openstackclient/tests/unit/volume/v2/test_type.py index d94dc1e08e..1cb46c4595 100644 --- a/openstackclient/tests/unit/volume/v2/test_type.py +++ b/openstackclient/tests/unit/volume/v2/test_type.py @@ -28,7 +28,7 @@ class TestType(volume_fakes.TestVolume): def setUp(self): - super(TestType, self).setUp() + super().setUp() self.types_mock = self.app.client_manager.volume.volume_types self.types_mock.reset_mock() @@ -56,10 +56,9 @@ class TestTypeCreate(TestType): ) def setUp(self): - super(TestTypeCreate, self).setUp() + super().setUp() - self.new_volume_type = \ - volume_fakes.FakeVolumeType.create_one_volume_type() + self.new_volume_type = volume_fakes.create_one_volume_type() self.data = ( self.new_volume_type.description, self.new_volume_type.id, @@ -144,12 +143,12 @@ def test_type_create_with_encryption(self): 'key_size': '128', 'control_location': 'front-end', } - encryption_type = \ - volume_fakes.FakeVolumeType.create_one_encryption_volume_type( - attrs=encryption_info) - self.new_volume_type = \ - volume_fakes.FakeVolumeType.create_one_volume_type( - attrs={'encryption': encryption_info}) + encryption_type = volume_fakes.create_one_encryption_volume_type( + attrs=encryption_info, + ) + self.new_volume_type = volume_fakes.create_one_volume_type( + attrs={'encryption': encryption_info}, + ) self.types_mock.create.return_value = self.new_volume_type self.encryption_types_mock.create.return_value = encryption_type encryption_columns = ( @@ -203,13 +202,14 @@ def test_type_create_with_encryption(self): class TestTypeDelete(TestType): - volume_types = volume_fakes.FakeVolumeType.create_volume_types(count=2) + volume_types = volume_fakes.create_volume_types(count=2) def setUp(self): - super(TestTypeDelete, self).setUp() + super().setUp() - self.types_mock.get = volume_fakes.FakeVolumeType.get_volume_types( - self.volume_types) + self.types_mock.get = volume_fakes.get_volume_types( + self.volume_types, + ) self.types_mock.delete.return_value = None # Get the command object to mock @@ -278,7 +278,7 @@ def test_delete_multiple_types_with_exception(self): class TestTypeList(TestType): - volume_types = volume_fakes.FakeVolumeType.create_volume_types() + volume_types = volume_fakes.create_volume_types() columns = [ "ID", @@ -312,7 +312,7 @@ class TestTypeList(TestType): )) def setUp(self): - super(TestTypeList, self).setUp() + super().setUp() self.types_mock.list.return_value = self.volume_types self.types_mock.default.return_value = self.volume_types[0] @@ -388,9 +388,9 @@ def test_type_list_with_default_option(self): self.assertCountEqual(self.data_with_default_type, list(data)) def test_type_list_with_encryption(self): - encryption_type = \ - volume_fakes.FakeVolumeType.create_one_encryption_volume_type( - attrs={'volume_type_id': self.volume_types[0].id}) + encryption_type = volume_fakes.create_one_encryption_volume_type( + attrs={'volume_type_id': self.volume_types[0].id}, + ) encryption_info = { 'provider': 'LuksEncryptor', 'cipher': None, @@ -436,11 +436,12 @@ def test_type_list_with_encryption(self): class TestTypeSet(TestType): project = identity_fakes.FakeProject.create_one_project() - volume_type = volume_fakes.FakeVolumeType.create_one_volume_type( - methods={'set_keys': None}) + volume_type = volume_fakes.create_one_volume_type( + methods={'set_keys': None}, + ) def setUp(self): - super(TestTypeSet, self).setUp() + super().setUp() self.types_mock.get.return_value = self.volume_type @@ -685,9 +686,9 @@ class TestTypeShow(TestType): ) def setUp(self): - super(TestTypeShow, self).setUp() + super().setUp() - self.volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() + self.volume_type = volume_fakes.create_one_volume_type() self.data = ( None, self.volume_type.description, @@ -727,13 +728,20 @@ def test_type_show_with_access(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - private_type = volume_fakes.FakeVolumeType.create_one_volume_type( - attrs={'is_public': False}) - type_access_list = volume_fakes.FakeTypeAccess.create_one_type_access() - with mock.patch.object(self.types_mock, 'get', - return_value=private_type): - with mock.patch.object(self.types_access_mock, 'list', - return_value=[type_access_list]): + private_type = volume_fakes.create_one_volume_type( + attrs={'is_public': False}, + ) + type_access_list = volume_fakes.create_one_type_access() + with mock.patch.object( + self.types_mock, + 'get', + return_value=private_type, + ): + with mock.patch.object( + self.types_access_mock, + 'list', + return_value=[type_access_list], + ): columns, data = self.cmd.take_action(parsed_args) self.types_mock.get.assert_called_once_with( self.volume_type.id) @@ -760,8 +768,9 @@ def test_type_show_with_list_access_exec(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - private_type = volume_fakes.FakeVolumeType.create_one_volume_type( - attrs={'is_public': False}) + private_type = volume_fakes.create_one_volume_type( + attrs={'is_public': False}, + ) with mock.patch.object(self.types_mock, 'get', return_value=private_type): with mock.patch.object(self.types_access_mock, 'list', @@ -784,16 +793,16 @@ def test_type_show_with_list_access_exec(self): self.assertCountEqual(private_type_data, data) def test_type_show_with_encryption(self): - encryption_type = \ - volume_fakes.FakeVolumeType.create_one_encryption_volume_type() + encryption_type = volume_fakes.create_one_encryption_volume_type() encryption_info = { 'provider': 'LuksEncryptor', 'cipher': None, 'key_size': None, 'control_location': 'front-end', } - self.volume_type = volume_fakes.FakeVolumeType.create_one_volume_type( - attrs={'encryption': encryption_info}) + self.volume_type = volume_fakes.create_one_volume_type( + attrs={'encryption': encryption_info}, + ) self.types_mock.get.return_value = self.volume_type self.encryption_types_mock.get.return_value = encryption_type encryption_columns = ( @@ -834,11 +843,12 @@ def test_type_show_with_encryption(self): class TestTypeUnset(TestType): project = identity_fakes.FakeProject.create_one_project() - volume_type = volume_fakes.FakeVolumeType.create_one_volume_type( - methods={'unset_keys': None}) + volume_type = volume_fakes.create_one_volume_type( + methods={'unset_keys': None}, + ) def setUp(self): - super(TestTypeUnset, self).setUp() + super().setUp() self.types_mock.get.return_value = self.volume_type @@ -936,7 +946,7 @@ def test_type_unset_encryption_type(self): class TestColumns(TestType): def test_encryption_info_column_with_info(self): - fake_volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() + fake_volume_type = volume_fakes.create_one_volume_type() type_id = fake_volume_type.id encryption_info = { @@ -952,7 +962,7 @@ def test_encryption_info_column_with_info(self): self.assertEqual(encryption_info, col.machine_readable()) def test_encryption_info_column_without_info(self): - fake_volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() + fake_volume_type = volume_fakes.create_one_volume_type() type_id = fake_volume_type.id col = volume_type.EncryptionInfoColumn(type_id, {}) diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index ec82e6746b..3d7d6d7300 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -30,7 +30,7 @@ class TestVolume(volume_fakes.TestVolume): def setUp(self): - super(TestVolume, self).setUp() + super().setUp() self.volumes_mock = self.app.client_manager.volume.volumes self.volumes_mock.reset_mock() @@ -55,11 +55,9 @@ def setUp(self): self.consistencygroups_mock.reset_mock() def setup_volumes_mock(self, count): - volumes = volume_fakes.FakeVolume.create_volumes(count=count) + volumes = volume_fakes.create_volumes(count=count) - self.volumes_mock.get = volume_fakes.FakeVolume.get_volumes( - volumes, - 0) + self.volumes_mock.get = volume_fakes.get_volumes(volumes, 0) return volumes @@ -83,9 +81,9 @@ class TestVolumeCreate(TestVolume): ) def setUp(self): - super(TestVolumeCreate, self).setUp() + super().setUp() - self.new_volume = volume_fakes.FakeVolume.create_one_volume() + self.new_volume = volume_fakes.create_one_volume() self.volumes_mock.create.return_value = self.new_volume self.datalist = ( @@ -139,8 +137,7 @@ def test_volume_create_min_options(self): self.assertCountEqual(self.datalist, data) def test_volume_create_options(self): - consistency_group = ( - volume_fakes.FakeConsistencyGroup.create_one_consistency_group()) + consistency_group = volume_fakes.create_one_consistency_group() self.consistencygroups_mock.get.return_value = consistency_group arglist = [ '--size', str(self.new_volume.size), @@ -297,7 +294,7 @@ def test_volume_create_image_name(self): self.assertCountEqual(self.datalist, data) def test_volume_create_with_snapshot(self): - snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() + snapshot = volume_fakes.create_one_snapshot() self.new_volume.snapshot_id = snapshot.id arglist = [ '--snapshot', self.new_volume.snapshot_id, @@ -504,7 +501,7 @@ def test_volume_create_with_multi_source(self): class TestVolumeDelete(TestVolume): def setUp(self): - super(TestVolumeDelete, self).setUp() + super().setUp() self.volumes_mock.delete.return_value = None @@ -632,9 +629,9 @@ class TestVolumeList(TestVolume): ] def setUp(self): - super(TestVolumeList, self).setUp() + super().setUp() - self.mock_volume = volume_fakes.FakeVolume.create_one_volume() + self.mock_volume = volume_fakes.create_one_volume() self.volumes_mock.list.return_value = [self.mock_volume] self.users_mock.get.return_value = self.user @@ -1107,10 +1104,10 @@ def test_volume_list_backward_compatibility(self): class TestVolumeMigrate(TestVolume): - _volume = volume_fakes.FakeVolume.create_one_volume() + _volume = volume_fakes.create_one_volume() def setUp(self): - super(TestVolumeMigrate, self).setUp() + super().setUp() self.volumes_mock.get.return_value = self._volume self.volumes_mock.migrate_volume.return_value = None @@ -1173,12 +1170,12 @@ def test_volume_migrate_without_host(self): class TestVolumeSet(TestVolume): - volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() + volume_type = volume_fakes.create_one_volume_type() def setUp(self): - super(TestVolumeSet, self).setUp() + super().setUp() - self.new_volume = volume_fakes.FakeVolume.create_one_volume() + self.new_volume = volume_fakes.create_one_volume() self.volumes_mock.get.return_value = self.new_volume self.types_mock.get.return_value = self.volume_type @@ -1427,9 +1424,9 @@ def test_volume_set_with_only_retype_policy(self, mock_warning): class TestVolumeShow(TestVolume): def setUp(self): - super(TestVolumeShow, self).setUp() + super().setUp() - self._volume = volume_fakes.FakeVolume.create_one_volume() + self._volume = volume_fakes.create_one_volume() self.volumes_mock.get.return_value = self._volume # Get the command object to test self.cmd = volume.ShowVolume(self.app, None) @@ -1447,20 +1444,21 @@ def test_volume_show(self): self.volumes_mock.get.assert_called_with(self._volume.id) self.assertEqual( - volume_fakes.FakeVolume.get_volume_columns(self._volume), - columns) - + volume_fakes.get_volume_columns(self._volume), + columns, + ) self.assertCountEqual( - volume_fakes.FakeVolume.get_volume_data(self._volume), - data) + volume_fakes.get_volume_data(self._volume), + data, + ) class TestVolumeUnset(TestVolume): def setUp(self): - super(TestVolumeUnset, self).setUp() + super().setUp() - self.new_volume = volume_fakes.FakeVolume.create_one_volume() + self.new_volume = volume_fakes.create_one_volume() self.volumes_mock.get.return_value = self.new_volume # Get the command object to set property @@ -1538,7 +1536,7 @@ def test_volume_unset_image_property_fail(self): class TestColumns(TestVolume): def test_attachments_column_without_server_cache(self): - _volume = volume_fakes.FakeVolume.create_one_volume() + _volume = volume_fakes.create_one_volume() server_id = _volume.attachments[0]['server_id'] device = _volume.attachments[0]['device'] @@ -1548,7 +1546,7 @@ def test_attachments_column_without_server_cache(self): self.assertEqual(_volume.attachments, col.machine_readable()) def test_attachments_column_with_server_cache(self): - _volume = volume_fakes.FakeVolume.create_one_volume() + _volume = volume_fakes.create_one_volume() server_id = _volume.attachments[0]['server_id'] device = _volume.attachments[0]['device'] diff --git a/openstackclient/tests/unit/volume/v2/test_volume_backend.py b/openstackclient/tests/unit/volume/v2/test_volume_backend.py index d9ac2c96c7..6c64f64558 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_backend.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_backend.py @@ -20,10 +20,10 @@ class TestShowVolumeCapability(volume_fakes.TestVolume): """Test backend capability functionality.""" # The capability to be listed - capability = volume_fakes.FakeCapability.create_one_capability() + capability = volume_fakes.create_one_capability() def setUp(self): - super(TestShowVolumeCapability, self).setUp() + super().setUp() # Get a shortcut to the capability Mock self.capability_mock = self.app.client_manager.volume.capabilities @@ -77,10 +77,10 @@ class TestListVolumePool(volume_fakes.TestVolume): """Tests for volume backend pool listing.""" # The pool to be listed - pools = volume_fakes.FakePool.create_one_pool() + pools = volume_fakes.create_one_pool() def setUp(self): - super(TestListVolumePool, self).setUp() + super().setUp() self.pool_mock = self.app.client_manager.volume.pools self.pool_mock.list.return_value = [self.pools] diff --git a/openstackclient/tests/unit/volume/v2/test_volume_backup.py b/openstackclient/tests/unit/volume/v2/test_volume_backup.py index ffd8490141..7d00b8bf81 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_backup.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_backup.py @@ -26,7 +26,7 @@ class TestBackup(volume_fakes.TestVolume): def setUp(self): - super(TestBackup, self).setUp() + super().setUp() self.backups_mock = self.app.client_manager.volume.backups self.backups_mock.reset_mock() @@ -40,9 +40,9 @@ def setUp(self): class TestBackupCreate(TestBackup): - volume = volume_fakes.FakeVolume.create_one_volume() - snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() - new_backup = volume_fakes.FakeBackup.create_one_backup( + volume = volume_fakes.create_one_volume() + snapshot = volume_fakes.create_one_snapshot() + new_backup = volume_fakes.create_one_backup( attrs={'volume_id': volume.id, 'snapshot_id': snapshot.id}) columns = ( @@ -71,7 +71,7 @@ class TestBackupCreate(TestBackup): ) def setUp(self): - super(TestBackupCreate, self).setUp() + super().setUp() self.volumes_mock.get.return_value = self.volume self.snapshots_mock.get.return_value = self.snapshot @@ -242,13 +242,13 @@ def test_backup_create_without_name(self): class TestBackupDelete(TestBackup): - backups = volume_fakes.FakeBackup.create_backups(count=2) + backups = volume_fakes.create_backups(count=2) def setUp(self): - super(TestBackupDelete, self).setUp() + super().setUp() self.backups_mock.get = ( - volume_fakes.FakeBackup.get_backups(self.backups)) + volume_fakes.get_backups(self.backups)) self.backups_mock.delete.return_value = None # Get the command object to mock @@ -334,8 +334,8 @@ def test_delete_multiple_backups_with_exception(self): class TestBackupList(TestBackup): - volume = volume_fakes.FakeVolume.create_one_volume() - backups = volume_fakes.FakeBackup.create_backups( + volume = volume_fakes.create_one_volume() + backups = volume_fakes.create_backups( attrs={'volume_id': volume.name}, count=3) columns = ( @@ -374,7 +374,7 @@ class TestBackupList(TestBackup): )) def setUp(self): - super(TestBackupList, self).setUp() + super().setUp() self.volumes_mock.list.return_value = [self.volume] self.backups_mock.list.return_value = self.backups @@ -456,8 +456,8 @@ def test_backup_list_with_options(self): class TestBackupRestore(TestBackup): - volume = volume_fakes.FakeVolume.create_one_volume() - backup = volume_fakes.FakeBackup.create_one_backup( + volume = volume_fakes.create_one_volume() + backup = volume_fakes.create_one_backup( attrs={'volume_id': volume.id}, ) @@ -467,7 +467,7 @@ def setUp(self): self.backups_mock.get.return_value = self.backup self.volumes_mock.get.return_value = self.volume self.restores_mock.restore.return_value = ( - volume_fakes.FakeVolume.create_one_volume( + volume_fakes.create_one_volume( {'id': self.volume['id']}, ) ) @@ -550,12 +550,12 @@ def test_backup_restore_with_volume_existing(self): class TestBackupSet(TestBackup): - backup = volume_fakes.FakeBackup.create_one_backup( + backup = volume_fakes.create_one_backup( attrs={'metadata': {'wow': 'cool'}}, ) def setUp(self): - super(TestBackupSet, self).setUp() + super().setUp() self.backups_mock.get.return_value = self.backup @@ -784,7 +784,7 @@ def test_backup_set_property_pre_v343(self): class TestBackupUnset(TestBackup): - backup = volume_fakes.FakeBackup.create_one_backup( + backup = volume_fakes.create_one_backup( attrs={'metadata': {'foo': 'bar'}}, ) @@ -845,7 +845,7 @@ def test_backup_unset_property_pre_v343(self): class TestBackupShow(TestBackup): - backup = volume_fakes.FakeBackup.create_one_backup() + backup = volume_fakes.create_one_backup() columns = ( 'availability_zone', @@ -873,7 +873,7 @@ class TestBackupShow(TestBackup): ) def setUp(self): - super(TestBackupShow, self).setUp() + super().setUp() self.backups_mock.get.return_value = self.backup # Get the command object to test diff --git a/openstackclient/tests/unit/volume/v2/test_volume_host.py b/openstackclient/tests/unit/volume/v2/test_volume_host.py index b024329a1b..730085a379 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_host.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_host.py @@ -12,14 +12,14 @@ # under the License. # -from openstackclient.tests.unit.volume.v2 import fakes as host_fakes +from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import volume_host -class TestVolumeHost(host_fakes.TestVolume): +class TestVolumeHost(volume_fakes.TestVolume): def setUp(self): - super(TestVolumeHost, self).setUp() + super().setUp() self.host_mock = self.app.client_manager.volume.services self.host_mock.reset_mock() @@ -27,10 +27,10 @@ def setUp(self): class TestVolumeHostSet(TestVolumeHost): - service = host_fakes.FakeService.create_one_service() + service = volume_fakes.create_one_service() def setUp(self): - super(TestVolumeHostSet, self).setUp() + super().setUp() self.host_mock.freeze_host.return_value = None self.host_mock.thaw_host.return_value = None @@ -89,10 +89,10 @@ def test_volume_host_set_disable(self): class TestVolumeHostFailover(TestVolumeHost): - service = host_fakes.FakeService.create_one_service() + service = volume_fakes.create_one_service() def setUp(self): - super(TestVolumeHostFailover, self).setUp() + super().setUp() self.host_mock.failover_host.return_value = None diff --git a/openstackclient/tests/unit/volume/v2/test_volume_snapshot.py b/openstackclient/tests/unit/volume/v2/test_volume_snapshot.py index 33a5a98a35..6cffcaaca9 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_snapshot.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_snapshot.py @@ -54,8 +54,8 @@ class TestVolumeSnapshotCreate(TestVolumeSnapshot): def setUp(self): super().setUp() - self.volume = volume_fakes.FakeVolume.create_one_volume() - self.new_snapshot = volume_fakes.FakeSnapshot.create_one_snapshot( + self.volume = volume_fakes.create_one_volume() + self.new_snapshot = volume_fakes.create_one_snapshot( attrs={'volume_id': self.volume.id}) self.data = ( @@ -179,13 +179,13 @@ def test_snapshot_create_with_remote_source(self): class TestVolumeSnapshotDelete(TestVolumeSnapshot): - snapshots = volume_fakes.FakeSnapshot.create_snapshots(count=2) + snapshots = volume_fakes.create_snapshots(count=2) def setUp(self): super().setUp() self.snapshots_mock.get = ( - volume_fakes.FakeSnapshot.get_snapshots(self.snapshots)) + volume_fakes.get_snapshots(self.snapshots)) self.snapshots_mock.delete.return_value = None # Get the command object to mock @@ -273,9 +273,9 @@ def test_delete_multiple_snapshots_with_exception(self): class TestVolumeSnapshotList(TestVolumeSnapshot): - volume = volume_fakes.FakeVolume.create_one_volume() + volume = volume_fakes.create_one_volume() project = project_fakes.FakeProject.create_one_project() - snapshots = volume_fakes.FakeSnapshot.create_snapshots( + snapshots = volume_fakes.create_snapshots( attrs={'volume_id': volume.name}, count=3) columns = [ @@ -495,7 +495,7 @@ def test_snapshot_list_negative_limit(self): class TestVolumeSnapshotSet(TestVolumeSnapshot): - snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() + snapshot = volume_fakes.create_one_snapshot() def setUp(self): super().setUp() @@ -677,7 +677,7 @@ class TestVolumeSnapshotShow(TestVolumeSnapshot): def setUp(self): super().setUp() - self.snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() + self.snapshot = volume_fakes.create_one_snapshot() self.data = ( self.snapshot.created_at, @@ -712,7 +712,7 @@ def test_snapshot_show(self): class TestVolumeSnapshotUnset(TestVolumeSnapshot): - snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() + snapshot = volume_fakes.create_one_snapshot() def setUp(self): super().setUp() diff --git a/openstackclient/tests/unit/volume/v2/test_volume_transfer_request.py b/openstackclient/tests/unit/volume/v2/test_volume_transfer_request.py index 1a1f220ff6..c8c6fac92b 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_transfer_request.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_transfer_request.py @@ -20,14 +20,14 @@ from osc_lib import utils from openstackclient.tests.unit import utils as test_utils -from openstackclient.tests.unit.volume.v2 import fakes as transfer_fakes +from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import volume_transfer_request -class TestTransfer(transfer_fakes.TestVolume): +class TestTransfer(volume_fakes.TestVolume): def setUp(self): - super(TestTransfer, self).setUp() + super().setUp() # Get a shortcut to the TransferManager Mock self.transfer_mock = self.app.client_manager.volume.transfers @@ -47,10 +47,9 @@ class TestTransferAccept(TestTransfer): ) def setUp(self): - super(TestTransferAccept, self).setUp() + super().setUp() - self.volume_transfer = ( - transfer_fakes.FakeTransfer.create_one_transfer()) + self.volume_transfer = volume_fakes.create_one_transfer() self.data = ( self.volume_transfer.id, self.volume_transfer.name, @@ -106,7 +105,7 @@ def test_transfer_accept_no_option(self): class TestTransferCreate(TestTransfer): - volume = transfer_fakes.FakeVolume.create_one_volume() + volume = volume_fakes.create_one_volume() columns = ( 'auth_key', @@ -117,12 +116,14 @@ class TestTransferCreate(TestTransfer): ) def setUp(self): - super(TestTransferCreate, self).setUp() - - self.volume_transfer = transfer_fakes.FakeTransfer.create_one_transfer( - attrs={'volume_id': self.volume.id, - 'auth_key': 'key', - 'created_at': 'time'} + super().setUp() + + self.volume_transfer = volume_fakes.create_one_transfer( + attrs={ + 'volume_id': self.volume.id, + 'auth_key': 'key', + 'created_at': 'time', + }, ) self.data = ( self.volume_transfer.auth_key, @@ -221,13 +222,14 @@ def test_transfer_create_pre_v355(self): class TestTransferDelete(TestTransfer): - volume_transfers = transfer_fakes.FakeTransfer.create_transfers(count=2) + volume_transfers = volume_fakes.create_transfers(count=2) def setUp(self): - super(TestTransferDelete, self).setUp() + super().setUp() - self.transfer_mock.get = ( - transfer_fakes.FakeTransfer.get_transfers(self.volume_transfers)) + self.transfer_mock.get = volume_fakes.get_transfers( + self.volume_transfers, + ) self.transfer_mock.delete.return_value = None # Get the command object to mock @@ -300,10 +302,10 @@ def test_delete_multiple_transfers_with_exception(self): class TestTransferList(TestTransfer): # The Transfers to be listed - volume_transfers = transfer_fakes.FakeTransfer.create_one_transfer() + volume_transfers = volume_fakes.create_one_transfer() def setUp(self): - super(TestTransferList, self).setUp() + super().setUp() self.transfer_mock.list.return_value = [self.volume_transfers] @@ -394,11 +396,10 @@ class TestTransferShow(TestTransfer): ) def setUp(self): - super(TestTransferShow, self).setUp() + super().setUp() - self.volume_transfer = ( - transfer_fakes.FakeTransfer.create_one_transfer( - attrs={'created_at': 'time'}) + self.volume_transfer = volume_fakes.create_one_transfer( + attrs={'created_at': 'time'}, ) self.data = ( self.volume_transfer.created_at, diff --git a/openstackclient/tests/unit/volume/v3/fakes.py b/openstackclient/tests/unit/volume/v3/fakes.py index caf7d2696f..3e3a05facc 100644 --- a/openstackclient/tests/unit/volume/v3/fakes.py +++ b/openstackclient/tests/unit/volume/v3/fakes.py @@ -24,7 +24,6 @@ class FakeVolumeClient: - def __init__(self, **kwargs): self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] @@ -51,17 +50,14 @@ def __init__(self, **kwargs): class TestVolume(utils.TestCommand): - def setUp(self): super().setUp() self.app.client_manager.volume = FakeVolumeClient( - endpoint=fakes.AUTH_URL, - token=fakes.AUTH_TOKEN + endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN ) self.app.client_manager.identity = identity_fakes.FakeIdentityv3Client( - endpoint=fakes.AUTH_URL, - token=fakes.AUTH_TOKEN + endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN ) self.app.client_manager.compute = compute_fakes.FakeComputev2Client( endpoint=fakes.AUTH_URL, @@ -70,353 +66,309 @@ def setUp(self): # TODO(stephenfin): Check if the responses are actually the same -FakeVolume = volume_v2_fakes.FakeVolume -FakeVolumeType = volume_v2_fakes.FakeVolumeType - - -class FakeCluster: - """Fake one or more clusters.""" - - @staticmethod - def create_one_cluster(attrs=None): - """Create a fake service cluster. - - :param attrs: A dictionary with all attributes of service cluster - :return: A FakeResource object with id, name, status, etc. - """ - attrs = attrs or {} - - # Set default attribute - cluster_info = { - 'name': f'cluster-{uuid.uuid4().hex}', - 'binary': f'binary-{uuid.uuid4().hex}', - 'state': random.choice(['up', 'down']), - 'status': random.choice(['enabled', 'disabled']), - 'disabled_reason': None, - 'num_hosts': random.randint(1, 64), - 'num_down_hosts': random.randint(1, 64), - 'last_heartbeat': '2015-09-16T09:28:52.000000', - 'created_at': '2015-09-16T09:28:52.000000', - 'updated_at': '2015-09-16T09:28:52.000000', - 'replication_status': None, - 'frozen': False, - 'active_backend_id': None, - } - - # Overwrite default attributes if there are some attributes set - cluster_info.update(attrs) - - return fakes.FakeResource( - None, - cluster_info, - loaded=True) - - @staticmethod - def create_clusters(attrs=None, count=2): - """Create multiple fake service clusters. - - :param attrs: A dictionary with all attributes of service cluster - :param count: The number of service clusters to be faked - :return: A list of FakeResource objects - """ - clusters = [] - for n in range(0, count): - clusters.append(FakeCluster.create_one_cluster(attrs)) - - return clusters - - -class FakeResourceFilter: - """Fake one or more resource filters.""" - - @staticmethod - def create_one_resource_filter(attrs=None): - """Create a fake resource filter. - - :param attrs: A dictionary with all attributes of resource filter - :return: A FakeResource object with id, name, status, etc. - """ - attrs = attrs or {} - - # Set default attribute - - resource_filter_info = { - 'filters': [ - 'name', - 'status', - 'image_metadata', - 'bootable', - 'migration_status', - ], - 'resource': 'volume', - } - - # Overwrite default attributes if there are some attributes set - resource_filter_info.update(attrs) - - return fakes.FakeResource(None, resource_filter_info, loaded=True) - - @staticmethod - def create_resource_filters(attrs=None, count=2): - """Create multiple fake resource filters. - - :param attrs: A dictionary with all attributes of resource filter - :param count: The number of resource filters to be faked - :return: A list of FakeResource objects - """ - resource_filters = [] - for n in range(0, count): - resource_filters.append( - FakeResourceFilter.create_one_resource_filter(attrs) - ) - - return resource_filters - - -class FakeVolumeGroup: - """Fake one or more volume groups.""" - - @staticmethod - def create_one_volume_group(attrs=None): - """Create a fake group. - - :param attrs: A dictionary with all attributes of group - :return: A FakeResource object with id, name, status, etc. - """ - attrs = attrs or {} - - group_type = attrs.pop('group_type', None) or uuid.uuid4().hex - volume_types = attrs.pop('volume_types', None) or [uuid.uuid4().hex] - - # Set default attribute - group_info = { - 'id': uuid.uuid4().hex, - 'status': random.choice([ +create_one_volume = volume_v2_fakes.create_one_volume +create_one_volume_type = volume_v2_fakes.create_one_volume_type + + +def create_one_cluster(attrs=None): + """Create a fake service cluster. + + :param attrs: A dictionary with all attributes of service cluster + :return: A FakeResource object with id, name, status, etc. + """ + attrs = attrs or {} + + # Set default attribute + cluster_info = { + 'name': f'cluster-{uuid.uuid4().hex}', + 'binary': f'binary-{uuid.uuid4().hex}', + 'state': random.choice(['up', 'down']), + 'status': random.choice(['enabled', 'disabled']), + 'disabled_reason': None, + 'num_hosts': random.randint(1, 64), + 'num_down_hosts': random.randint(1, 64), + 'last_heartbeat': '2015-09-16T09:28:52.000000', + 'created_at': '2015-09-16T09:28:52.000000', + 'updated_at': '2015-09-16T09:28:52.000000', + 'replication_status': None, + 'frozen': False, + 'active_backend_id': None, + } + + # Overwrite default attributes if there are some attributes set + cluster_info.update(attrs) + + return fakes.FakeResource(None, cluster_info, loaded=True) + + +def create_clusters(attrs=None, count=2): + """Create multiple fake service clusters. + + :param attrs: A dictionary with all attributes of service cluster + :param count: The number of service clusters to be faked + :return: A list of FakeResource objects + """ + clusters = [] + for n in range(0, count): + clusters.append(create_one_cluster(attrs)) + + return clusters + + +def create_one_resource_filter(attrs=None): + """Create a fake resource filter. + + :param attrs: A dictionary with all attributes of resource filter + :return: A FakeResource object with id, name, status, etc. + """ + attrs = attrs or {} + + # Set default attribute + + resource_filter_info = { + 'filters': [ + 'name', + 'status', + 'image_metadata', + 'bootable', + 'migration_status', + ], + 'resource': 'volume', + } + + # Overwrite default attributes if there are some attributes set + resource_filter_info.update(attrs) + + return fakes.FakeResource(None, resource_filter_info, loaded=True) + + +def create_resource_filters(attrs=None, count=2): + """Create multiple fake resource filters. + + :param attrs: A dictionary with all attributes of resource filter + :param count: The number of resource filters to be faked + :return: A list of FakeResource objects + """ + resource_filters = [] + for n in range(0, count): + resource_filters.append(create_one_resource_filter(attrs)) + + return resource_filters + + +def create_one_volume_group(attrs=None): + """Create a fake group. + + :param attrs: A dictionary with all attributes of group + :return: A FakeResource object with id, name, status, etc. + """ + attrs = attrs or {} + + group_type = attrs.pop('group_type', None) or uuid.uuid4().hex + volume_types = attrs.pop('volume_types', None) or [uuid.uuid4().hex] + + # Set default attribute + group_info = { + 'id': uuid.uuid4().hex, + 'status': random.choice( + [ 'available', - ]), - 'availability_zone': f'az-{uuid.uuid4().hex}', - 'created_at': '2015-09-16T09:28:52.000000', - 'name': 'first_group', - 'description': f'description-{uuid.uuid4().hex}', - 'group_type': group_type, - 'volume_types': volume_types, - 'volumes': [f'volume-{uuid.uuid4().hex}'], - 'group_snapshot_id': None, - 'source_group_id': None, - 'project_id': f'project-{uuid.uuid4().hex}', - } - - # Overwrite default attributes if there are some attributes set - group_info.update(attrs) - - group = fakes.FakeResource( - None, - group_info, - loaded=True) - return group - - @staticmethod - def create_volume_groups(attrs=None, count=2): - """Create multiple fake groups. - - :param attrs: A dictionary with all attributes of group - :param count: The number of groups to be faked - :return: A list of FakeResource objects - """ - groups = [] - for n in range(0, count): - groups.append(FakeVolumeGroup.create_one_volume_group(attrs)) - - return groups - - -class FakeVolumeGroupSnapshot: - """Fake one or more volume group snapshots.""" - - @staticmethod - def create_one_volume_group_snapshot(attrs=None, methods=None): - """Create a fake group snapshot. - - :param attrs: A dictionary with all attributes - :param methods: A dictionary with all methods - :return: A FakeResource object with id, name, description, etc. - """ - attrs = attrs or {} - - # Set default attribute - group_snapshot_info = { - 'id': uuid.uuid4().hex, - 'name': f'group-snapshot-{uuid.uuid4().hex}', - 'description': f'description-{uuid.uuid4().hex}', - 'status': random.choice(['available']), - 'group_id': uuid.uuid4().hex, - 'group_type_id': uuid.uuid4().hex, - 'project_id': uuid.uuid4().hex, - } - - # Overwrite default attributes if there are some attributes set - group_snapshot_info.update(attrs) - - group_snapshot = fakes.FakeResource( - None, - group_snapshot_info, - methods=methods, - loaded=True) - return group_snapshot - - @staticmethod - def create_volume_group_snapshots(attrs=None, count=2): - """Create multiple fake group snapshots. - - :param attrs: A dictionary with all attributes of group snapshot - :param count: The number of group snapshots to be faked - :return: A list of FakeResource objects - """ - group_snapshots = [] - for n in range(0, count): - group_snapshots.append( - FakeVolumeGroupSnapshot.create_one_volume_group_snapshot(attrs) - ) - - return group_snapshots - - -class FakeVolumeGroupType: - """Fake one or more volume group types.""" - - @staticmethod - def create_one_volume_group_type(attrs=None, methods=None): - """Create a fake group type. - - :param attrs: A dictionary with all attributes of group type - :param methods: A dictionary with all methods - :return: A FakeResource object with id, name, description, etc. - """ - attrs = attrs or {} - - # Set default attribute - group_type_info = { - 'id': uuid.uuid4().hex, - 'name': f'group-type-{uuid.uuid4().hex}', - 'description': f'description-{uuid.uuid4().hex}', - 'is_public': random.choice([True, False]), - 'group_specs': {}, - } - - # Overwrite default attributes if there are some attributes set - group_type_info.update(attrs) - - group_type = fakes.FakeResource( - None, - group_type_info, - methods=methods, - loaded=True) - return group_type - - @staticmethod - def create_volume_group_types(attrs=None, count=2): - """Create multiple fake group types. - - :param attrs: A dictionary with all attributes of group type - :param count: The number of group types to be faked - :return: A list of FakeResource objects - """ - group_types = [] - for n in range(0, count): - group_types.append( - FakeVolumeGroupType.create_one_volume_group_type(attrs) - ) - - return group_types - - -class FakeVolumeMessage: - """Fake one or more volume messages.""" - - @staticmethod - def create_one_volume_message(attrs=None): - """Create a fake message. - - :param attrs: A dictionary with all attributes of message - :return: A FakeResource object with id, name, status, etc. - """ - attrs = attrs or {} - - # Set default attribute - message_info = { - 'created_at': '2016-02-11T11:17:37.000000', - 'event_id': f'VOLUME_{random.randint(1, 999999):06d}', - 'guaranteed_until': '2016-02-11T11:17:37.000000', - 'id': uuid.uuid4().hex, - 'message_level': 'ERROR', - 'request_id': f'req-{uuid.uuid4().hex}', - 'resource_type': 'VOLUME', - 'resource_uuid': uuid.uuid4().hex, - 'user_message': f'message-{uuid.uuid4().hex}', - } - - # Overwrite default attributes if there are some attributes set - message_info.update(attrs) - - return fakes.FakeResource( - None, - message_info, - loaded=True) - - @staticmethod - def create_volume_messages(attrs=None, count=2): - """Create multiple fake messages. - - :param attrs: A dictionary with all attributes of message - :param count: The number of messages to be faked - :return: A list of FakeResource objects - """ - messages = [] - for n in range(0, count): - messages.append(FakeVolumeMessage.create_one_volume_message(attrs)) - - return messages - - @staticmethod - def get_volume_messages(messages=None, count=2): - """Get an iterable MagicMock object with a list of faked messages. - - If messages list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param messages: A list of FakeResource objects faking messages - :param count: The number of messages to be faked - :return An iterable Mock object with side_effect set to a list of faked - messages - """ - if messages is None: - messages = FakeVolumeMessage.create_messages(count) - - return mock.Mock(side_effect=messages) - - -class FakeVolumeAttachment: - """Fake one or more volume attachments.""" - - @staticmethod - def create_one_volume_attachment(attrs=None): - """Create a fake volume attachment. - - :param attrs: A dictionary with all attributes of volume attachment - :return: A FakeResource object with id, status, etc. - """ - attrs = attrs or {} - - attachment_id = uuid.uuid4().hex - volume_id = attrs.pop('volume_id', None) or uuid.uuid4().hex - server_id = attrs.pop('instance', None) or uuid.uuid4().hex - - # Set default attribute - attachment_info = { - 'id': attachment_id, - 'volume_id': volume_id, - 'instance': server_id, - 'status': random.choice([ + ] + ), + 'availability_zone': f'az-{uuid.uuid4().hex}', + 'created_at': '2015-09-16T09:28:52.000000', + 'name': 'first_group', + 'description': f'description-{uuid.uuid4().hex}', + 'group_type': group_type, + 'volume_types': volume_types, + 'volumes': [f'volume-{uuid.uuid4().hex}'], + 'group_snapshot_id': None, + 'source_group_id': None, + 'project_id': f'project-{uuid.uuid4().hex}', + } + + # Overwrite default attributes if there are some attributes set + group_info.update(attrs) + + group = fakes.FakeResource(None, group_info, loaded=True) + return group + + +def create_volume_groups(attrs=None, count=2): + """Create multiple fake groups. + + :param attrs: A dictionary with all attributes of group + :param count: The number of groups to be faked + :return: A list of FakeResource objects + """ + groups = [] + for n in range(0, count): + groups.append(create_one_volume_group(attrs)) + + return groups + + +def create_one_volume_group_snapshot(attrs=None, methods=None): + """Create a fake group snapshot. + + :param attrs: A dictionary with all attributes + :param methods: A dictionary with all methods + :return: A FakeResource object with id, name, description, etc. + """ + attrs = attrs or {} + + # Set default attribute + group_snapshot_info = { + 'id': uuid.uuid4().hex, + 'name': f'group-snapshot-{uuid.uuid4().hex}', + 'description': f'description-{uuid.uuid4().hex}', + 'status': random.choice(['available']), + 'group_id': uuid.uuid4().hex, + 'group_type_id': uuid.uuid4().hex, + 'project_id': uuid.uuid4().hex, + } + + # Overwrite default attributes if there are some attributes set + group_snapshot_info.update(attrs) + + group_snapshot = fakes.FakeResource( + None, group_snapshot_info, methods=methods, loaded=True + ) + return group_snapshot + + +def create_volume_group_snapshots(attrs=None, count=2): + """Create multiple fake group snapshots. + + :param attrs: A dictionary with all attributes of group snapshot + :param count: The number of group snapshots to be faked + :return: A list of FakeResource objects + """ + group_snapshots = [] + for n in range(0, count): + group_snapshots.append(create_one_volume_group_snapshot(attrs)) + + return group_snapshots + + +def create_one_volume_group_type(attrs=None, methods=None): + """Create a fake group type. + + :param attrs: A dictionary with all attributes of group type + :param methods: A dictionary with all methods + :return: A FakeResource object with id, name, description, etc. + """ + attrs = attrs or {} + + # Set default attribute + group_type_info = { + 'id': uuid.uuid4().hex, + 'name': f'group-type-{uuid.uuid4().hex}', + 'description': f'description-{uuid.uuid4().hex}', + 'is_public': random.choice([True, False]), + 'group_specs': {}, + } + + # Overwrite default attributes if there are some attributes set + group_type_info.update(attrs) + + group_type = fakes.FakeResource( + None, group_type_info, methods=methods, loaded=True + ) + return group_type + + +def create_volume_group_types(attrs=None, count=2): + """Create multiple fake group types. + + :param attrs: A dictionary with all attributes of group type + :param count: The number of group types to be faked + :return: A list of FakeResource objects + """ + group_types = [] + for n in range(0, count): + group_types.append(create_one_volume_group_type(attrs)) + + return group_types + + +def create_one_volume_message(attrs=None): + """Create a fake message. + + :param attrs: A dictionary with all attributes of message + :return: A FakeResource object with id, name, status, etc. + """ + attrs = attrs or {} + + # Set default attribute + message_info = { + 'created_at': '2016-02-11T11:17:37.000000', + 'event_id': f'VOLUME_{random.randint(1, 999999):06d}', + 'guaranteed_until': '2016-02-11T11:17:37.000000', + 'id': uuid.uuid4().hex, + 'message_level': 'ERROR', + 'request_id': f'req-{uuid.uuid4().hex}', + 'resource_type': 'VOLUME', + 'resource_uuid': uuid.uuid4().hex, + 'user_message': f'message-{uuid.uuid4().hex}', + } + + # Overwrite default attributes if there are some attributes set + message_info.update(attrs) + + return fakes.FakeResource(None, message_info, loaded=True) + + +def create_volume_messages(attrs=None, count=2): + """Create multiple fake messages. + + :param attrs: A dictionary with all attributes of message + :param count: The number of messages to be faked + :return: A list of FakeResource objects + """ + messages = [] + for n in range(0, count): + messages.append(create_one_volume_message(attrs)) + + return messages + + +def get_volume_messages(messages=None, count=2): + """Get an iterable MagicMock object with a list of faked messages. + + If messages list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param messages: A list of FakeResource objects faking messages + :param count: The number of messages to be faked + :return An iterable Mock object with side_effect set to a list of faked + messages + """ + if messages is None: + messages = create_volume_messages(count) + + return mock.Mock(side_effect=messages) + + +def create_one_volume_attachment(attrs=None): + """Create a fake volume attachment. + + :param attrs: A dictionary with all attributes of volume attachment + :return: A FakeResource object with id, status, etc. + """ + attrs = attrs or {} + + attachment_id = uuid.uuid4().hex + volume_id = attrs.pop('volume_id', None) or uuid.uuid4().hex + server_id = attrs.pop('instance', None) or uuid.uuid4().hex + + # Set default attribute + attachment_info = { + 'id': attachment_id, + 'volume_id': volume_id, + 'instance': server_id, + 'status': random.choice( + [ 'attached', 'attaching', 'detached', @@ -424,67 +376,63 @@ def create_one_volume_attachment(attrs=None): 'error_attaching', 'error_detaching', 'deleted', - ]), - 'attach_mode': random.choice(['ro', 'rw']), - 'attached_at': '2015-09-16T09:28:52.000000', - 'detached_at': None, - 'connection_info': { - 'access_mode': 'rw', - 'attachment_id': attachment_id, - 'auth_method': 'CHAP', - 'auth_password': 'AcUZ8PpxLHwzypMC', - 'auth_username': '7j3EZQWT3rbE6pcSGKvK', - 'cacheable': False, - 'driver_volume_type': 'iscsi', - 'encrypted': False, - 'qos_specs': None, - 'target_discovered': False, - 'target_iqn': - f'iqn.2010-10.org.openstack:volume-{attachment_id}', - 'target_lun': '1', - 'target_portal': '192.168.122.170:3260', - 'volume_id': volume_id, - }, - } - - # Overwrite default attributes if there are some attributes set - attachment_info.update(attrs) - - return fakes.FakeResource( - None, - attachment_info, - loaded=True) - - @staticmethod - def create_volume_attachments(attrs=None, count=2): - """Create multiple fake volume attachments. - - :param attrs: A dictionary with all attributes of volume attachment - :param count: The number of volume attachments to be faked - :return: A list of FakeResource objects - """ - attachments = [] - - for n in range(0, count): - attachments.append( - FakeVolumeAttachment.create_one_volume_attachment(attrs)) - - return attachments - - @staticmethod - def get_volume_attachments(attachments=None, count=2): - """Get an iterable MagicMock object with a list of faked volumes. - - If attachments list is provided, then initialize the Mock object with - the list. Otherwise create one. - - :param attachments: A list of FakeResource objects faking volume - attachments - :param count: The number of volume attachments to be faked - :return An iterable Mock object with side_effect set to a list of faked - volume attachments - """ - if attachments is None: - attachments = FakeVolumeAttachment.create_volume_attachments(count) - - return mock.Mock(side_effect=attachments) + ] + ), + 'attach_mode': random.choice(['ro', 'rw']), + 'attached_at': '2015-09-16T09:28:52.000000', + 'detached_at': None, + 'connection_info': { + 'access_mode': 'rw', + 'attachment_id': attachment_id, + 'auth_method': 'CHAP', + 'auth_password': 'AcUZ8PpxLHwzypMC', + 'auth_username': '7j3EZQWT3rbE6pcSGKvK', + 'cacheable': False, + 'driver_volume_type': 'iscsi', + 'encrypted': False, + 'qos_specs': None, + 'target_discovered': False, + 'target_iqn': f'iqn.2010-10.org.openstack:volume-{attachment_id}', + 'target_lun': '1', + 'target_portal': '192.168.122.170:3260', + 'volume_id': volume_id, + }, + } + + # Overwrite default attributes if there are some attributes set + attachment_info.update(attrs) + + return fakes.FakeResource(None, attachment_info, loaded=True) + + +def create_volume_attachments(attrs=None, count=2): + """Create multiple fake volume attachments. + + :param attrs: A dictionary with all attributes of volume attachment + :param count: The number of volume attachments to be faked + :return: A list of FakeResource objects + """ + attachments = [] + + for n in range(0, count): + attachments.append(create_one_volume_attachment(attrs)) + + return attachments + + +def get_volume_attachments(attachments=None, count=2): + """Get an iterable MagicMock object with a list of faked volumes. + + If attachments list is provided, then initialize the Mock object with + the list. Otherwise create one. + + :param attachments: A list of FakeResource objects faking volume + attachments + :param count: The number of volume attachments to be faked + :return An iterable Mock object with side_effect set to a list of faked + volume attachments + """ + if attachments is None: + attachments = create_volume_attachments(count) + + return mock.Mock(side_effect=attachments) diff --git a/openstackclient/tests/unit/volume/v3/test_block_storage_cluster.py b/openstackclient/tests/unit/volume/v3/test_block_storage_cluster.py index d87a946b9f..fdfd110084 100644 --- a/openstackclient/tests/unit/volume/v3/test_block_storage_cluster.py +++ b/openstackclient/tests/unit/volume/v3/test_block_storage_cluster.py @@ -30,7 +30,7 @@ def setUp(self): class TestBlockStorageClusterList(TestBlockStorageCluster): # The cluster to be listed - fake_clusters = volume_fakes.FakeCluster.create_clusters() + fake_clusters = volume_fakes.create_clusters() def setUp(self): super().setUp() @@ -176,7 +176,7 @@ def test_cluster_list_pre_v37(self): class TestBlockStorageClusterSet(TestBlockStorageCluster): - cluster = volume_fakes.FakeCluster.create_one_cluster() + cluster = volume_fakes.create_one_cluster() columns = ( 'Name', 'Binary', @@ -347,7 +347,7 @@ def test_cluster_set_pre_v37(self): class TestBlockStorageClusterShow(TestBlockStorageCluster): - cluster = volume_fakes.FakeCluster.create_one_cluster() + cluster = volume_fakes.create_one_cluster() columns = ( 'Name', 'Binary', diff --git a/openstackclient/tests/unit/volume/v3/test_block_storage_resource_filter.py b/openstackclient/tests/unit/volume/v3/test_block_storage_resource_filter.py index b886726dd5..086339ffd1 100644 --- a/openstackclient/tests/unit/volume/v3/test_block_storage_resource_filter.py +++ b/openstackclient/tests/unit/volume/v3/test_block_storage_resource_filter.py @@ -31,8 +31,7 @@ def setUp(self): class TestBlockStorageResourceFilterList(TestBlockStorageResourceFilter): # The resource filters to be listed - fake_resource_filters = \ - volume_fakes.FakeResourceFilter.create_resource_filters() + fake_resource_filters = volume_fakes.create_resource_filters() def setUp(self): super().setUp() @@ -86,8 +85,7 @@ def test_resource_filter_list_pre_v333(self): class TestBlockStorageResourceFilterShow(TestBlockStorageResourceFilter): # The resource filters to be listed - fake_resource_filter = \ - volume_fakes.FakeResourceFilter.create_one_resource_filter() + fake_resource_filter = volume_fakes.create_one_resource_filter() def setUp(self): super().setUp() diff --git a/openstackclient/tests/unit/volume/v3/test_volume_attachment.py b/openstackclient/tests/unit/volume/v3/test_volume_attachment.py index 44fac6c586..c0bf5ae783 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume_attachment.py +++ b/openstackclient/tests/unit/volume/v3/test_volume_attachment.py @@ -41,11 +41,11 @@ def setUp(self): class TestVolumeAttachmentCreate(TestVolumeAttachment): - volume = volume_fakes.FakeVolume.create_one_volume() + volume = volume_fakes.create_one_volume() server = compute_fakes.FakeServer.create_one_server() - volume_attachment = \ - volume_fakes.FakeVolumeAttachment.create_one_volume_attachment( - attrs={'instance': server.id, 'volume_id': volume.id}) + volume_attachment = volume_fakes.create_one_volume_attachment( + attrs={'instance': server.id, 'volume_id': volume.id}, + ) columns = ( 'ID', @@ -238,8 +238,7 @@ def test_volume_attachment_create_with_connect_missing_arg(self): class TestVolumeAttachmentDelete(TestVolumeAttachment): - volume_attachment = \ - volume_fakes.FakeVolumeAttachment.create_one_volume_attachment() + volume_attachment = volume_fakes.create_one_volume_attachment() def setUp(self): super().setUp() @@ -290,8 +289,7 @@ def test_volume_attachment_delete_pre_v327(self): class TestVolumeAttachmentSet(TestVolumeAttachment): - volume_attachment = \ - volume_fakes.FakeVolumeAttachment.create_one_volume_attachment() + volume_attachment = volume_fakes.create_one_volume_attachment() columns = ( 'ID', @@ -391,8 +389,7 @@ def test_volume_attachment_set_pre_v327(self): class TestVolumeAttachmentComplete(TestVolumeAttachment): - volume_attachment = \ - volume_fakes.FakeVolumeAttachment.create_one_volume_attachment() + volume_attachment = volume_fakes.create_one_volume_attachment() def setUp(self): super().setUp() @@ -444,8 +441,7 @@ def test_volume_attachment_complete_pre_v344(self): class TestVolumeAttachmentList(TestVolumeAttachment): project = identity_fakes.FakeProject.create_one_project() - volume_attachments = \ - volume_fakes.FakeVolumeAttachment.create_volume_attachments() + volume_attachments = volume_fakes.create_volume_attachments() columns = ( 'ID', diff --git a/openstackclient/tests/unit/volume/v3/test_volume_group.py b/openstackclient/tests/unit/volume/v3/test_volume_group.py index 13ef38d208..96079a0848 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume_group.py +++ b/openstackclient/tests/unit/volume/v3/test_volume_group.py @@ -35,10 +35,9 @@ def setUp(self): class TestVolumeGroupCreate(TestVolumeGroup): - fake_volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() - fake_volume_group_type = \ - volume_fakes.FakeVolumeGroupType.create_one_volume_group_type() - fake_volume_group = volume_fakes.FakeVolumeGroup.create_one_volume_group( + fake_volume_type = volume_fakes.create_one_volume_type() + fake_volume_group_type = volume_fakes.create_one_volume_group_type() + fake_volume_group = volume_fakes.create_one_volume_group( attrs={ 'group_type': fake_volume_group_type.id, 'volume_types': [fake_volume_type.id], @@ -180,8 +179,7 @@ def test_volume_group_create_pre_v313(self): class TestVolumeGroupDelete(TestVolumeGroup): - fake_volume_group = \ - volume_fakes.FakeVolumeGroup.create_one_volume_group() + fake_volume_group = volume_fakes.create_one_volume_group() def setUp(self): super().setUp() @@ -236,8 +234,7 @@ def test_volume_group_delete_pre_v313(self): class TestVolumeGroupSet(TestVolumeGroup): - fake_volume_group = \ - volume_fakes.FakeVolumeGroup.create_one_volume_group() + fake_volume_group = volume_fakes.create_one_volume_group() columns = ( 'ID', @@ -368,8 +365,7 @@ def test_volume_group_with_enable_replication_option_pre_v338(self): class TestVolumeGroupList(TestVolumeGroup): - fake_volume_groups = \ - volume_fakes.FakeVolumeGroup.create_volume_groups() + fake_volume_groups = volume_fakes.create_volume_groups() columns = ( 'ID', @@ -436,8 +432,7 @@ def test_volume_group_list_pre_v313(self): class TestVolumeGroupFailover(TestVolumeGroup): - fake_volume_group = \ - volume_fakes.FakeVolumeGroup.create_one_volume_group() + fake_volume_group = volume_fakes.create_one_volume_group() def setUp(self): super().setUp() diff --git a/openstackclient/tests/unit/volume/v3/test_volume_group_snapshot.py b/openstackclient/tests/unit/volume/v3/test_volume_group_snapshot.py index 509d9f08f2..2a5a30f07e 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume_group_snapshot.py +++ b/openstackclient/tests/unit/volume/v3/test_volume_group_snapshot.py @@ -32,9 +32,9 @@ def setUp(self): class TestVolumeGroupSnapshotCreate(TestVolumeGroupSnapshot): - fake_volume_group = volume_fakes.FakeVolumeGroup.create_one_volume_group() + fake_volume_group = volume_fakes.create_one_volume_group() fake_volume_group_snapshot = \ - volume_fakes.FakeVolumeGroupSnapshot.create_one_volume_group_snapshot() + volume_fakes.create_one_volume_group_snapshot() columns = ( 'ID', @@ -141,7 +141,7 @@ def test_volume_group_snapshot_create_pre_v314(self): class TestVolumeGroupSnapshotDelete(TestVolumeGroupSnapshot): fake_volume_group_snapshot = \ - volume_fakes.FakeVolumeGroupSnapshot.create_one_volume_group_snapshot() + volume_fakes.create_one_volume_group_snapshot() def setUp(self): super().setUp() @@ -195,8 +195,7 @@ def test_volume_group_snapshot_delete_pre_v314(self): class TestVolumeGroupSnapshotList(TestVolumeGroupSnapshot): - fake_volume_group_snapshots = \ - volume_fakes.FakeVolumeGroupSnapshot.create_volume_group_snapshots() + fake_volume_group_snapshots = volume_fakes.create_volume_group_snapshots() columns = ( 'ID', diff --git a/openstackclient/tests/unit/volume/v3/test_volume_group_type.py b/openstackclient/tests/unit/volume/v3/test_volume_group_type.py index 7e758a2c1f..34b4e501fb 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume_group_type.py +++ b/openstackclient/tests/unit/volume/v3/test_volume_group_type.py @@ -34,8 +34,7 @@ class TestVolumeGroupTypeCreate(TestVolumeGroupType): maxDiff = 2000 - fake_volume_group_type = \ - volume_fakes.FakeVolumeGroupType.create_one_volume_group_type() + fake_volume_group_type = volume_fakes.create_one_volume_group_type() columns = ( 'ID', @@ -133,8 +132,7 @@ def test_volume_group_type_create_pre_v311(self): class TestVolumeGroupTypeDelete(TestVolumeGroupType): - fake_volume_group_type = \ - volume_fakes.FakeVolumeGroupType.create_one_volume_group_type() + fake_volume_group_type = volume_fakes.create_one_volume_group_type() def setUp(self): super().setUp() @@ -187,13 +185,13 @@ def test_volume_group_type_delete_pre_v311(self): class TestVolumeGroupTypeSet(TestVolumeGroupType): - fake_volume_group_type = \ - volume_fakes.FakeVolumeGroupType.create_one_volume_group_type( - methods={ - 'get_keys': {'foo': 'bar'}, - 'set_keys': None, - 'unset_keys': None, - }) + fake_volume_group_type = volume_fakes.create_one_volume_group_type( + methods={ + 'get_keys': {'foo': 'bar'}, + 'set_keys': None, + 'unset_keys': None, + }, + ) columns = ( 'ID', @@ -316,9 +314,9 @@ def test_volume_group_type_set_pre_v311(self): class TestVolumeGroupTypeUnset(TestVolumeGroupType): - fake_volume_group_type = \ - volume_fakes.FakeVolumeGroupType.create_one_volume_group_type( - methods={'unset_keys': None}) + fake_volume_group_type = volume_fakes.create_one_volume_group_type( + methods={'unset_keys': None}, + ) columns = ( 'ID', @@ -393,8 +391,7 @@ def test_volume_group_type_unset_pre_v311(self): class TestVolumeGroupTypeList(TestVolumeGroupType): - fake_volume_group_types = \ - volume_fakes.FakeVolumeGroupType.create_volume_group_types() + fake_volume_group_types = volume_fakes.create_volume_group_types() columns = ( 'ID', diff --git a/openstackclient/tests/unit/volume/v3/test_volume_message.py b/openstackclient/tests/unit/volume/v3/test_volume_message.py index 8cabc0c3ad..45b0747e1f 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume_message.py +++ b/openstackclient/tests/unit/volume/v3/test_volume_message.py @@ -34,15 +34,14 @@ def setUp(self): class TestVolumeMessageDelete(TestVolumeMessage): - fake_messages = volume_fakes.FakeVolumeMessage.create_volume_messages( - count=2) + fake_messages = volume_fakes.create_volume_messages(count=2) def setUp(self): super().setUp() - self.volume_messages_mock.get = \ - volume_fakes.FakeVolumeMessage.get_volume_messages( - self.fake_messages) + self.volume_messages_mock.get = volume_fakes.get_volume_messages( + self.fake_messages, + ) self.volume_messages_mock.delete.return_value = None # Get the command object to mock @@ -139,8 +138,7 @@ def test_message_delete_pre_v33(self): class TestVolumeMessageList(TestVolumeMessage): fake_project = identity_fakes.FakeProject.create_one_project() - fake_messages = volume_fakes.FakeVolumeMessage.create_volume_messages( - count=3) + fake_messages = volume_fakes.create_volume_messages(count=3) columns = ( 'ID', @@ -253,7 +251,7 @@ def test_message_list_pre_v33(self): class TestVolumeMessageShow(TestVolumeMessage): - fake_message = volume_fakes.FakeVolumeMessage.create_one_volume_message() + fake_message = volume_fakes.create_one_volume_message() columns = ( 'created_at', From 2da4aa99aaca3debcda1d046b7806a8f83ac8090 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 7 Oct 2022 17:12:39 +0100 Subject: [PATCH 040/595] quota: Fix issues with delete quota command We were passing a project object rather than just the ID. Also correct a typo in the call to delete network quotas. Change-Id: I2292db7932ec01026f0e54014e3d02218792617a Signed-off-by: Stephen Finucane --- openstackclient/common/quota.py | 6 ++--- .../tests/unit/common/test_quota.py | 27 ++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 10f40a75d4..f2d097fc17 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -972,12 +972,12 @@ def take_action(self, parsed_args): # compute quotas if parsed_args.service in {'all', 'compute'}: compute_client = self.app.client_manager.compute - compute_client.quotas.delete(project) + compute_client.quotas.delete(project.id) # volume quotas if parsed_args.service in {'all', 'volume'}: volume_client = self.app.client_manager.volume - volume_client.quotas.delete(project) + volume_client.quotas.delete(project.id) # network quotas (but only if we're not using nova-network, otherwise # we already deleted the quotas in the compute step) @@ -986,6 +986,6 @@ def take_action(self, parsed_args): and self.app.client_manager.is_network_endpoint_enabled() ): network_client = self.app.client_manager.network - network_client.quotas.delete(project) + network_client.delete_quota(project.id) return None diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index 682799b39e..f798137aeb 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -62,8 +62,8 @@ def setUp(self): self.app.client_manager.volume.quota_classes self.volume_quotas_class_mock.reset_mock() - self.app.client_manager.network.quotas = mock.Mock() - self.network_quotas_mock = self.app.client_manager.network.quotas + self.app.client_manager.network = mock.Mock() + self.network_mock = self.app.client_manager.network self.app.client_manager.auth_ref = mock.Mock() self.app.client_manager.auth_ref.service_catalog = mock.Mock() @@ -660,7 +660,6 @@ def setUp(self): loaded=True, ) - self.network_mock = self.app.client_manager.network self.network_mock.update_quota = mock.Mock() self.cmd = quota.SetQuota(self.app, None) @@ -1272,6 +1271,8 @@ class TestQuotaDelete(TestQuota): def setUp(self): super().setUp() + self.network_mock.delete_quota = mock.Mock() + self.cmd = quota.DeleteQuota(self.app, None) def test_delete(self): @@ -1291,13 +1292,13 @@ def test_delete(self): self.assertIsNone(result) self.projects_mock.get.assert_called_once_with(self.projects[0].id) self.compute_quotas_mock.delete.assert_called_once_with( - self.projects[0], + self.projects[0].id, ) self.volume_quotas_mock.delete.assert_called_once_with( - self.projects[0], + self.projects[0].id, ) - self.network_quotas_mock.delete.assert_called_once_with( - self.projects[0], + self.network_mock.delete_quota.assert_called_once_with( + self.projects[0].id, ) def test_delete__compute(self): @@ -1318,10 +1319,10 @@ def test_delete__compute(self): self.assertIsNone(result) self.projects_mock.get.assert_called_once_with(self.projects[0].id) self.compute_quotas_mock.delete.assert_called_once_with( - self.projects[0], + self.projects[0].id, ) self.volume_quotas_mock.delete.assert_not_called() - self.network_quotas_mock.delete.assert_not_called() + self.network_mock.delete_quota.assert_not_called() def test_delete__volume(self): """Delete volume quotas only""" @@ -1342,9 +1343,9 @@ def test_delete__volume(self): self.projects_mock.get.assert_called_once_with(self.projects[0].id) self.compute_quotas_mock.delete.assert_not_called() self.volume_quotas_mock.delete.assert_called_once_with( - self.projects[0], + self.projects[0].id, ) - self.network_quotas_mock.delete.assert_not_called() + self.network_mock.delete_quota.assert_not_called() def test_delete__network(self): """Delete network quotas only""" @@ -1365,6 +1366,6 @@ def test_delete__network(self): self.projects_mock.get.assert_called_once_with(self.projects[0].id) self.compute_quotas_mock.delete.assert_not_called() self.volume_quotas_mock.delete.assert_not_called() - self.network_quotas_mock.delete.assert_called_once_with( - self.projects[0], + self.network_mock.delete_quota.assert_called_once_with( + self.projects[0].id, ) From 09ff9a0f4c118f50924d2fc078d6a4501e696224 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 29 Nov 2021 11:26:13 +0000 Subject: [PATCH 041/595] quota: Deprecate "force" behavior for network quotas In change Idc1b99492d609eb699d0a6bef6cd760458a774f6, we added a '--check-limit' option to enable "no force" behavior for network quotas. This was already the default for compute quotas. Provide a path for harmonizing the behavior of the two options by instead using a '--no-force' option which will become the eventual default for the network quotas also. Change-Id: I25828a3d68e2e900f498e17a0d01fb70be77548e Signed-off-by: Stephen Finucane --- openstackclient/common/quota.py | 51 +++++++++++++++---- .../tests/functional/common/test_quota.py | 4 +- .../tests/unit/common/test_quota.py | 7 +-- ...no-force-limit-quota-cc7f291dd1b537c1.yaml | 13 +++++ 4 files changed, 59 insertions(+), 16 deletions(-) create mode 100644 releasenotes/notes/no-force-limit-quota-cc7f291dd1b537c1.yaml diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 10f40a75d4..510437ddd5 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -15,6 +15,7 @@ """Quota action implementations""" +import argparse import itertools import logging import sys @@ -621,20 +622,37 @@ def get_parser(self, prog_name): metavar='', help=_('Set quotas for a specific '), ) - parser.add_argument( + force_group = parser.add_mutually_exclusive_group() + force_group.add_argument( '--force', action='store_true', + dest='force', + # TODO(stephenfin): Change the default to False in Z or later + default=None, help=_( - 'Force quota update (only supported by compute and network)' + 'Force quota update (only supported by compute and network) ' + '(default for network)' ), ) - parser.add_argument( - '--check-limit', - action='store_true', + force_group.add_argument( + '--no-force', + action='store_false', + dest='force', + default=None, help=_( - 'Check quota limit when updating (only supported by network)' + 'Do not force quota update ' + '(only supported by compute and network) ' + '(default for compute)' ), ) + # kept here for backwards compatibility/to keep the neutron folks happy + force_group.add_argument( + '--check-limit', + action='store_false', + dest='force', + default=None, + help=argparse.SUPPRESS, + ) return parser def take_action(self, parsed_args): @@ -657,8 +675,8 @@ def take_action(self, parsed_args): if value is not None: compute_kwargs[k] = value - if parsed_args.force: - compute_kwargs['force'] = True + if parsed_args.force is not None: + compute_kwargs['force'] = parsed_args.force volume_kwargs = {} for k, v in VOLUME_QUOTAS.items(): @@ -669,10 +687,21 @@ def take_action(self, parsed_args): volume_kwargs[k] = value network_kwargs = {} - if parsed_args.check_limit: - network_kwargs['check_limit'] = True - if parsed_args.force: + if parsed_args.force is True: + # Unlike compute, network doesn't provide a simple boolean option. + # Instead, it provides two options: 'force' and 'check_limit' + # (a.k.a. 'not force') network_kwargs['force'] = True + elif parsed_args.force is False: + network_kwargs['check_limit'] = True + else: + msg = _( + "This command currently defaults to '--force' when modifying " + "network quotas. This behavior will change in a future " + "release. Consider explicitly providing '--force' or " + "'--no-force' options to avoid changes in behavior." + ) + self.log.warning(msg) if self.app.client_manager.is_network_endpoint_enabled(): for k, v in NETWORK_QUOTAS.items(): diff --git a/openstackclient/tests/functional/common/test_quota.py b/openstackclient/tests/functional/common/test_quota.py index 08ec626f86..9089cba5c0 100644 --- a/openstackclient/tests/functional/common/test_quota.py +++ b/openstackclient/tests/functional/common/test_quota.py @@ -176,7 +176,7 @@ def test_quota_set_class(self): def _restore_quota_limit(self, resource, limit, project): self.openstack('quota set --%s %s %s' % (resource, limit, project)) - def test_quota_network_set_with_check_limit(self): + def test_quota_network_set_with_no_force(self): if not self.haz_network: self.skipTest('No Network service present') if not self.is_extension_enabled('quota-check-limit'): @@ -201,7 +201,7 @@ def test_quota_network_set_with_check_limit(self): (self.PROJECT_NAME, uuid.uuid4().hex)) self.assertRaises(exceptions.CommandFailed, self.openstack, - 'quota set --networks 1 --check-limit ' + + 'quota set --networks 1 --no-force ' + self.PROJECT_NAME) def test_quota_network_set_with_force(self): diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index 0900200e96..320e6b51b3 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -985,19 +985,19 @@ def test_quota_set_with_force(self): ) self.assertIsNone(result) - def test_quota_set_with_check_limit(self): + def test_quota_set_with_no_force(self): arglist = [ '--subnets', str(network_fakes.QUOTA['subnet']), '--volumes', str(volume_fakes.QUOTA['volumes']), '--cores', str(compute_fakes.core_num), - '--check-limit', + '--no-force', self.projects[0].name, ] verifylist = [ ('subnet', network_fakes.QUOTA['subnet']), ('volumes', volume_fakes.QUOTA['volumes']), ('cores', compute_fakes.core_num), - ('check_limit', True), + ('force', False), ('project', self.projects[0].name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1006,6 +1006,7 @@ def test_quota_set_with_check_limit(self): kwargs_compute = { 'cores': compute_fakes.core_num, + 'force': False, } kwargs_volume = { 'volumes': volume_fakes.QUOTA['volumes'], diff --git a/releasenotes/notes/no-force-limit-quota-cc7f291dd1b537c1.yaml b/releasenotes/notes/no-force-limit-quota-cc7f291dd1b537c1.yaml new file mode 100644 index 0000000000..53b7eb8d3b --- /dev/null +++ b/releasenotes/notes/no-force-limit-quota-cc7f291dd1b537c1.yaml @@ -0,0 +1,13 @@ +--- +features: + - Add ``--no-force`` option to the ``openstack quota set`` command (only + for compute and network commands). When specified, the compute and network + quota engine will check the resource usage before setting the new quota + limit. This is the default behavior of the compute quota engine and will + become the default for the network quota engine in a future release. +deprecations: + - The ``openstack quota set`` command currently defaults to ``--force`` + behavior for network quotas. This behavior is now deprecated and a future + release will switch to ``--no-force`` behavior. Users should explicitly + specify one of these options to prevent a potentially breaking change in + behavior. From ce4cbeab67f6dbf87b0f15dabcff43488eca0050 Mon Sep 17 00:00:00 2001 From: Daniel Wilson Date: Tue, 18 Oct 2022 00:30:55 -0400 Subject: [PATCH 042/595] Use the compute SDK in usage commands Update usage list and usage show to use the compute component of the OpenStack SDK instead of directly using the nova interface. Change-Id: I1c4d2247c9c1a577ed9efad7e8332e7c9b974ad5 --- openstackclient/compute/v2/usage.py | 69 ++++++++----------- .../tests/unit/compute/v2/test_usage.py | 45 +++++------- 2 files changed, 48 insertions(+), 66 deletions(-) diff --git a/openstackclient/compute/v2/usage.py b/openstackclient/compute/v2/usage.py index 69fa04e8ba..86f538a7d9 100644 --- a/openstackclient/compute/v2/usage.py +++ b/openstackclient/compute/v2/usage.py @@ -15,12 +15,10 @@ """Usage action implementations""" -import collections import datetime import functools from cliff import columns as cliff_columns -from novaclient import api_versions from osc_lib.command import command from osc_lib import utils @@ -58,7 +56,7 @@ def human_readable(self): class CountColumn(cliff_columns.FormattableColumn): def human_readable(self): - return len(self._value) + return len(self._value) if self._value is not None else None class FloatColumn(cliff_columns.FormattableColumn): @@ -69,7 +67,7 @@ def human_readable(self): def _formatters(project_cache): return { - 'tenant_id': functools.partial( + 'project_id': functools.partial( ProjectColumn, project_cache=project_cache), 'server_usages': CountColumn, 'total_memory_mb_usage': FloatColumn, @@ -102,10 +100,10 @@ def _merge_usage(usage, next_usage): def _merge_usage_list(usages, next_usage_list): for next_usage in next_usage_list: - if next_usage.tenant_id in usages: - _merge_usage(usages[next_usage.tenant_id], next_usage) + if next_usage.project_id in usages: + _merge_usage(usages[next_usage.project_id], next_usage) else: - usages[next_usage.tenant_id] = next_usage + usages[next_usage.project_id] = next_usage class ListUsage(command.Lister): @@ -138,9 +136,9 @@ def _format_project(project): else: return project - compute_client = self.app.client_manager.compute + compute_client = self.app.client_manager.sdk_connection.compute columns = ( - "tenant_id", + "project_id", "server_usages", "total_memory_mb_usage", "total_vcpus_usage", @@ -154,36 +152,25 @@ def _format_project(project): "Disk GB-Hours" ) - dateformat = "%Y-%m-%d" + date_cli_format = "%Y-%m-%d" + date_api_format = "%Y-%m-%dT%H:%M:%S" now = datetime.datetime.utcnow() if parsed_args.start: - start = datetime.datetime.strptime(parsed_args.start, dateformat) + start = datetime.datetime.strptime( + parsed_args.start, date_cli_format) else: start = now - datetime.timedelta(weeks=4) if parsed_args.end: - end = datetime.datetime.strptime(parsed_args.end, dateformat) + end = datetime.datetime.strptime(parsed_args.end, date_cli_format) else: end = now + datetime.timedelta(days=1) - if compute_client.api_version < api_versions.APIVersion("2.40"): - usage_list = compute_client.usage.list(start, end, detailed=True) - else: - # If the number of instances used to calculate the usage is greater - # than CONF.api.max_limit, the usage will be split across multiple - # requests and the responses will need to be merged back together. - usages = collections.OrderedDict() - usage_list = compute_client.usage.list(start, end, detailed=True) - _merge_usage_list(usages, usage_list) - marker = _get_usage_list_marker(usage_list) - while marker: - next_usage_list = compute_client.usage.list( - start, end, detailed=True, marker=marker) - marker = _get_usage_list_marker(next_usage_list) - if marker: - _merge_usage_list(usages, next_usage_list) - usage_list = list(usages.values()) + usage_list = list(compute_client.usages( + start=start.strftime(date_api_format), + end=end.strftime(date_api_format), + detailed=True)) # Cache the project list project_cache = {} @@ -196,8 +183,8 @@ def _format_project(project): if parsed_args.formatter == 'table' and len(usage_list) > 0: self.app.stdout.write(_("Usage from %(start)s to %(end)s: \n") % { - "start": start.strftime(dateformat), - "end": end.strftime(dateformat), + "start": start.strftime(date_cli_format), + "end": end.strftime(date_cli_format), }) return ( @@ -239,17 +226,19 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): identity_client = self.app.client_manager.identity - compute_client = self.app.client_manager.compute - dateformat = "%Y-%m-%d" + compute_client = self.app.client_manager.sdk_connection.compute + date_cli_format = "%Y-%m-%d" + date_api_format = "%Y-%m-%dT%H:%M:%S" now = datetime.datetime.utcnow() if parsed_args.start: - start = datetime.datetime.strptime(parsed_args.start, dateformat) + start = datetime.datetime.strptime( + parsed_args.start, date_cli_format) else: start = now - datetime.timedelta(weeks=4) if parsed_args.end: - end = datetime.datetime.strptime(parsed_args.end, dateformat) + end = datetime.datetime.strptime(parsed_args.end, date_cli_format) else: end = now + datetime.timedelta(days=1) @@ -262,19 +251,21 @@ def take_action(self, parsed_args): # Get the project from the current auth project = self.app.client_manager.auth_ref.project_id - usage = compute_client.usage.get(project, start, end) + usage = compute_client.get_usage( + project=project, start=start.strftime(date_api_format), + end=end.strftime(date_api_format)) if parsed_args.formatter == 'table': self.app.stdout.write(_( "Usage from %(start)s to %(end)s on project %(project)s: \n" ) % { - "start": start.strftime(dateformat), - "end": end.strftime(dateformat), + "start": start.strftime(date_cli_format), + "end": end.strftime(date_cli_format), "project": project, }) columns = ( - "tenant_id", + "project_id", "server_usages", "total_memory_mb_usage", "total_vcpus_usage", diff --git a/openstackclient/tests/unit/compute/v2/test_usage.py b/openstackclient/tests/unit/compute/v2/test_usage.py index bbccb9bdc7..85b45e1b28 100644 --- a/openstackclient/tests/unit/compute/v2/test_usage.py +++ b/openstackclient/tests/unit/compute/v2/test_usage.py @@ -11,11 +11,8 @@ # under the License. # -import datetime from unittest import mock -from novaclient import api_versions - from openstackclient.compute.v2 import usage as usage_cmds from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes @@ -26,8 +23,9 @@ class TestUsage(compute_fakes.TestComputev2): def setUp(self): super(TestUsage, self).setUp() - self.usage_mock = self.app.client_manager.compute.usage - self.usage_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.projects_mock = self.app.client_manager.identity.projects self.projects_mock.reset_mock() @@ -38,7 +36,7 @@ class TestUsageList(TestUsage): project = identity_fakes.FakeProject.create_one_project() # Return value of self.usage_mock.list(). usages = compute_fakes.FakeUsage.create_usages( - attrs={'tenant_id': project.name}, count=1) + attrs={'project_id': project.name}, count=1) columns = ( "Project", @@ -49,7 +47,7 @@ class TestUsageList(TestUsage): ) data = [( - usage_cmds.ProjectColumn(usages[0].tenant_id), + usage_cmds.ProjectColumn(usages[0].project_id), usage_cmds.CountColumn(usages[0].server_usages), usage_cmds.FloatColumn(usages[0].total_memory_mb_usage), usage_cmds.FloatColumn(usages[0].total_vcpus_usage), @@ -59,7 +57,7 @@ class TestUsageList(TestUsage): def setUp(self): super(TestUsageList, self).setUp() - self.usage_mock.list.return_value = self.usages + self.sdk_client.usages.return_value = self.usages self.projects_mock.list.return_value = [self.project] # Get the command object to test @@ -97,9 +95,9 @@ def test_usage_list_with_options(self): columns, data = self.cmd.take_action(parsed_args) self.projects_mock.list.assert_called_with() - self.usage_mock.list.assert_called_with( - datetime.datetime(2016, 11, 11, 0, 0), - datetime.datetime(2016, 12, 20, 0, 0), + self.sdk_client.usages.assert_called_with( + start='2016-11-11T00:00:00', + end='2016-12-20T00:00:00', detailed=True) self.assertCountEqual(self.columns, columns) @@ -112,20 +110,13 @@ def test_usage_list_with_pagination(self): ('end', None), ] - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.40') - self.usage_mock.list.reset_mock() - self.usage_mock.list.side_effect = [self.usages, []] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.projects_mock.list.assert_called_with() - self.usage_mock.list.assert_has_calls([ - mock.call(mock.ANY, mock.ANY, detailed=True), - mock.call(mock.ANY, mock.ANY, detailed=True, - marker=self.usages[0]['server_usages'][0]['instance_id']) + self.sdk_client.usages.assert_has_calls([ + mock.call(start=mock.ANY, end=mock.ANY, detailed=True) ]) self.assertCountEqual(self.columns, columns) self.assertCountEqual(tuple(self.data), tuple(data)) @@ -136,7 +127,7 @@ class TestUsageShow(TestUsage): project = identity_fakes.FakeProject.create_one_project() # Return value of self.usage_mock.list(). usage = compute_fakes.FakeUsage.create_one_usage( - attrs={'tenant_id': project.name}) + attrs={'project_id': project.name}) columns = ( 'Project', @@ -147,7 +138,7 @@ class TestUsageShow(TestUsage): ) data = ( - usage_cmds.ProjectColumn(usage.tenant_id), + usage_cmds.ProjectColumn(usage.project_id), usage_cmds.CountColumn(usage.server_usages), usage_cmds.FloatColumn(usage.total_memory_mb_usage), usage_cmds.FloatColumn(usage.total_vcpus_usage), @@ -157,7 +148,7 @@ class TestUsageShow(TestUsage): def setUp(self): super(TestUsageShow, self).setUp() - self.usage_mock.get.return_value = self.usage + self.sdk_client.get_usage.return_value = self.usage self.projects_mock.get.return_value = self.project # Get the command object to test @@ -199,10 +190,10 @@ def test_usage_show_with_options(self): columns, data = self.cmd.take_action(parsed_args) - self.usage_mock.get.assert_called_with( - self.project.id, - datetime.datetime(2016, 11, 11, 0, 0), - datetime.datetime(2016, 12, 20, 0, 0)) + self.sdk_client.get_usage.assert_called_with( + project=self.project.id, + start='2016-11-11T00:00:00', + end='2016-12-20T00:00:00') self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) From a726d84f4b7cec761d3d82824997969dffbd4249 Mon Sep 17 00:00:00 2001 From: Areg Grigoryan Date: Thu, 25 Aug 2022 10:14:13 +0200 Subject: [PATCH 043/595] Added "openstack image metadefs namespace list" command Change-Id: Ibc064566478818bd70e7e6550095d24a05e1bb0e Depends-On: https://review.opendev.org/c/openstack/openstacksdk/+/854612 --- .../image/v2/metadef_namespaces.py | 65 ++++++++++++++++++ openstackclient/tests/unit/image/v2/fakes.py | 54 +++++++++++++++ .../unit/image/v2/test_metadef_namespaces.py | 67 +++++++++++++++++++ ...ef-namespace-support-4ba37ec3a1a72185.yaml | 4 ++ requirements.txt | 2 +- setup.cfg | 1 + 6 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 openstackclient/image/v2/metadef_namespaces.py create mode 100644 openstackclient/tests/unit/image/v2/test_metadef_namespaces.py create mode 100644 releasenotes/notes/add-image-metadef-namespace-support-4ba37ec3a1a72185.yaml diff --git a/openstackclient/image/v2/metadef_namespaces.py b/openstackclient/image/v2/metadef_namespaces.py new file mode 100644 index 0000000000..158fd94e3b --- /dev/null +++ b/openstackclient/image/v2/metadef_namespaces.py @@ -0,0 +1,65 @@ +# Copyright 2012-2013 OpenStack Foundation +# +# 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. +# + +"""Image V2 Action Implementations""" + +from osc_lib.cli import format_columns +from osc_lib.command import command +from osc_lib import utils + +from openstackclient.i18n import _ + +_formatters = { + 'tags': format_columns.ListColumn, +} + + +class ListMetadefNameSpaces(command.Lister): + _description = _("List metadef namespaces") + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + "--resource-types", + metavar="", + help=_("filter resource types"), + ) + parser.add_argument( + "--visibility", + metavar="", + help=_("filter on visibility"), + ) + return parser + + def take_action(self, parsed_args): + image_client = self.app.client_manager.image + filter_keys = ['resource_types', 'visibility'] + kwargs = {} + for key in filter_keys: + argument = getattr(parsed_args, key, None) + if argument is not None: + kwargs[key] = argument + # List of namespace data received + data = list(image_client.metadef_namespaces(**kwargs)) + columns = ['namespace'] + column_headers = columns + return ( + column_headers, + (utils.get_item_properties( + s, + columns, + formatters=_formatters, + ) for s in data) + ) diff --git a/openstackclient/tests/unit/image/v2/fakes.py b/openstackclient/tests/unit/image/v2/fakes.py index f20154502e..cf09df778a 100644 --- a/openstackclient/tests/unit/image/v2/fakes.py +++ b/openstackclient/tests/unit/image/v2/fakes.py @@ -18,6 +18,7 @@ from openstack.image.v2 import image from openstack.image.v2 import member +from openstack.image.v2 import metadef_namespace from openstack.image.v2 import task from openstackclient.tests.unit import fakes @@ -44,6 +45,7 @@ def __init__(self, **kwargs): self.update_member = mock.Mock() self.remove_tag = mock.Mock() + self.metadef_namespaces = mock.Mock() self.tasks = mock.Mock() self.get_task = mock.Mock() @@ -55,6 +57,8 @@ def __init__(self, **kwargs): self.tasks = mock.Mock() self.tasks.resource_class = fakes.FakeResource(None, {}) + self.metadef_namespaces = mock.Mock() + class TestImagev2(utils.TestCommand): @@ -202,3 +206,53 @@ def create_tasks(attrs=None, count=2): tasks.append(create_one_task(attrs)) return tasks + + +class FakeMetadefNamespaceClient: + + def __init__(self, **kwargs): + self.metadef_namespaces = mock.Mock() + + self.auth_token = kwargs['token'] + self.management_url = kwargs['endpoint'] + self.version = 2.0 + + +class TestMetadefNamespaces(utils.TestCommand): + + def setUp(self): + super().setUp() + + self.app.client_manager.image = FakeMetadefNamespaceClient( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + + self.app.client_manager.identity = identity_fakes.FakeIdentityv3Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + + +def create_one_metadef_namespace(attrs=None): + """Create a fake MetadefNamespace member. + + :param attrs: A dictionary with all attributes of metadef_namespace member + :type attrs: dict + :return: a list of MetadefNamespace objects + :rtype: list of `metadef_namespace.MetadefNamespace` + """ + attrs = attrs or {} + + metadef_namespace_list = { + 'created_at': '2022-08-17T11:30:22Z', + 'display_name': 'Flavor Quota', + 'namespace': 'OS::Compute::Quota', + 'owner': 'admin', + 'resource_type_associations': ['OS::Nova::Flavor'], + 'visibility': 'public', + } + + # Overwrite default attributes if there are some attributes set + metadef_namespace_list.update(attrs) + 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 new file mode 100644 index 0000000000..5eae289cdc --- /dev/null +++ b/openstackclient/tests/unit/image/v2/test_metadef_namespaces.py @@ -0,0 +1,67 @@ +# Copyright 2013 Nebula Inc. +# +# 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. + +from openstackclient.image.v2 import metadef_namespaces +from openstackclient.tests.unit.image.v2 import fakes as md_namespace_fakes + + +class TestMetadefNamespaces(md_namespace_fakes.TestMetadefNamespaces): + def setUp(self): + super().setUp() + + # Get shortcuts to mocked image client + self.client = self.app.client_manager.image + + # Get shortcut to the Mocks in identity client + self.project_mock = self.app.client_manager.identity.projects + self.project_mock.reset_mock() + self.domain_mock = self.app.client_manager.identity.domains + self.domain_mock.reset_mock() + + +class TestMetadefNamespaceList(TestMetadefNamespaces): + + _metadef_namespace = [md_namespace_fakes.create_one_metadef_namespace()] + + columns = [ + 'namespace' + ] + + datalist = [] + + def setUp(self): + super().setUp() + + self.client.metadef_namespaces.side_effect = [ + self._metadef_namespace, []] + + # Get the command object to test + self.client.metadef_namespaces.return_value = iter( + self._metadef_namespace + ) + self.cmd = metadef_namespaces.ListMetadefNameSpaces(self.app, None) + self.datalist = self._metadef_namespace + + def test_namespace_list_no_options(self): + arglist = [] + parsed_args = self.check_parser(self.cmd, arglist, []) + + # 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) + + self.assertEqual(self.columns, columns) + self.assertEqual(getattr(self.datalist[0], 'namespace'), + next(data)[0]) diff --git a/releasenotes/notes/add-image-metadef-namespace-support-4ba37ec3a1a72185.yaml b/releasenotes/notes/add-image-metadef-namespace-support-4ba37ec3a1a72185.yaml new file mode 100644 index 0000000000..c8a53ad038 --- /dev/null +++ b/releasenotes/notes/add-image-metadef-namespace-support-4ba37ec3a1a72185.yaml @@ -0,0 +1,4 @@ +--- +features: + - Adds ``openstack image metadef namespace list``. + The output is equivalent to glance md-namespace-list. diff --git a/requirements.txt b/requirements.txt index d14c324610..c787ef7627 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 cliff>=3.5.0 # Apache-2.0 iso8601>=0.1.11 # MIT -openstacksdk>=0.61.0 # Apache-2.0 +openstacksdk>=0.102.0 # Apache-2.0 osc-lib>=2.3.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index bd153d6ee3..f8d0dffce5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -385,6 +385,7 @@ openstack.image.v2 = image_unset = openstackclient.image.v2.image:UnsetImage image_task_show = openstackclient.image.v2.task:ShowTask image_task_list = openstackclient.image.v2.task:ListTask + image_metadef_namespace_list = openstackclient.image.v2.metadef_namespaces:ListMetadefNameSpaces openstack.network.v2 = address_group_create = openstackclient.network.v2.address_group:CreateAddressGroup From e76609650fd322cca6eb899aa6125c0450727b7c Mon Sep 17 00:00:00 2001 From: Artom Lifshitz Date: Wed, 26 Oct 2022 09:49:55 -0400 Subject: [PATCH 044/595] Improve `server dump create` helptext The `server dump create` command instructs Nova to trigger a crash dump in the guest OS. Assuming the guest supports this, the resulting dump file will be located in the guest, in a location dependent on the guest OS. Explain all that in the helptext. Story: 2010384 Change-Id: If940ed5cce6c5ab4193ab1494738149370da9aad --- openstackclient/compute/v2/server.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 1d0c03a270..01559b4286 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1756,8 +1756,9 @@ class CreateServerDump(command.Command): Trigger crash dump in server(s) with features like kdump in Linux. It will create a dump file in the server(s) dumping the server(s)' - memory, and also crash the server(s). OSC sees the dump file - (server dump) as a kind of resource. + memory, and also crash the server(s). This is contingent on guest operating + system support, and the location of the dump file inside the guest will + depend on the exact guest operating system. This command requires ``--os-compute-api-version`` 2.17 or greater. """ From a7975c42003d7df2af91154007435cd5f8560f24 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Oct 2022 13:24:30 +0300 Subject: [PATCH 045/595] compute: Add '--no-network', '--auto-network' flags These are aliases for '--nic none' and '--nic auto', respectively. Change-Id: I7b4f7e5c3769a813bd8b2b9cd6090c6fe501e13d Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 110 ++++++++++++++---- .../tests/unit/compute/v2/test_server.py | 58 ++++++--- ...o-no-network-options-f4ddb2bb7544d2f5.yaml | 6 + 3 files changed, 136 insertions(+), 38 deletions(-) create mode 100644 releasenotes/notes/auto-no-network-options-f4ddb2bb7544d2f5.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 1d0c03a270..39b2bdc81e 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -409,8 +409,8 @@ def get_parser(self, prog_name): '--tag', metavar='', help=_( - "Tag for the attached interface. " - "(Supported by API versions '2.49' - '2.latest')" + 'Tag for the attached interface ' + '(supported by --os-compute-api-version 2.49 or later)' ) ) return parser @@ -652,29 +652,68 @@ def take_action(self, parsed_args): ) -# TODO(stephenfin): Replace with 'MultiKeyValueAction' when we no longer -# support '--nic=auto' and '--nic=none' +class NoneNICAction(argparse.Action): + + def __init__(self, option_strings, dest, help=None): + super().__init__( + option_strings=option_strings, + dest=dest, + nargs=0, + default=[], + required=False, + help=help, + ) + + def __call__(self, parser, namespace, values, option_string=None): + # Make sure we have an empty dict rather than None + if getattr(namespace, self.dest, None) is None: + setattr(namespace, self.dest, []) + + getattr(namespace, self.dest).append('none') + + +class AutoNICAction(argparse.Action): + + def __init__(self, option_strings, dest, help=None): + super().__init__( + option_strings=option_strings, + dest=dest, + nargs=0, + default=[], + required=False, + help=help, + ) + + def __call__(self, parser, namespace, values, option_string=None): + # Make sure we have an empty dict rather than None + if getattr(namespace, self.dest, None) is None: + setattr(namespace, self.dest, []) + + getattr(namespace, self.dest).append('auto') + + class NICAction(argparse.Action): def __init__( self, option_strings, dest, - nargs=None, - const=None, - default=None, - type=None, - choices=None, - required=False, help=None, metavar=None, key=None, ): self.key = key super().__init__( - option_strings=option_strings, dest=dest, nargs=nargs, const=const, - default=default, type=type, choices=choices, required=required, - help=help, metavar=metavar, + option_strings=option_strings, + dest=dest, + nargs=None, + const=None, + default=[], + type=None, + choices=None, + required=False, + help=help, + metavar=metavar, ) def __call__(self, parser, namespace, values, option_string=None): @@ -707,7 +746,7 @@ def __call__(self, parser, namespace, values, option_string=None): } for kv_str in values.split(','): - k, sep, v = kv_str.partition("=") + k, sep, v = kv_str.partition('=') if k not in list(info) + ['tag'] or not v: msg = _( @@ -998,28 +1037,23 @@ def get_parser(self, prog_name): ) parser.add_argument( '--network', - metavar="", + metavar='', dest='nics', - default=[], action=NICAction, key='net-id', - # NOTE(RuiChen): Add '\n' to the end of line to improve formatting; - # see cliff's _SmartHelpFormatter for more details. help=_( "Create a NIC on the server and connect it to network. " "Specify option multiple times to create multiple NICs. " "This is a wrapper for the '--nic net-id=' " "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." + "For more advanced use cases, refer to the '--nic' parameter." ), ) parser.add_argument( '--port', - metavar="", + metavar='', dest='nics', - default=[], action=NICAction, key='port-id', help=_( @@ -1031,13 +1065,41 @@ def get_parser(self, prog_name): "more advanced use cases, refer to the '--nic' parameter." ), ) + parser.add_argument( + '--no-network', + dest='nics', + action=NoneNICAction, + help=_( + "Do not attach a network to the server. " + "This is a wrapper for the '--nic none' option that provides " + "a simple syntax for disabling network connectivity for a new " + "server. " + "For more advanced use cases, refer to the '--nic' parameter. " + "(supported by --os-compute-api-version 2.37 or above)" + ), + ) + parser.add_argument( + '--auto-network', + dest='nics', + action=AutoNICAction, + help=_( + "Automatically allocate a network to the server. " + "This is the default network allocation policy. " + "This is a wrapper for the '--nic auto' option that provides " + "a simple syntax for enabling automatic configuration of " + "network connectivity for a new server. " + "For more advanced use cases, refer to the '--nic' parameter. " + "(supported by --os-compute-api-version 2.37 or above)" + ), + ) parser.add_argument( '--nic', metavar="", - action=NICAction, dest='nics', - default=[], + action=NICAction, + # NOTE(RuiChen): Add '\n' to the end of line to improve formatting; + # see cliff's _SmartHelpFormatter for more details. help=_( "Create a NIC on the server.\n" "NIC in the format:\n" diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 2e64e071e5..f4fd44b17b 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -1900,13 +1900,7 @@ def test_server_create_with_network_tag_pre_v243(self): self.assertRaises( exceptions.CommandError, self.cmd.take_action, parsed_args) - def test_server_create_with_auto_network(self): - arglist = [ - '--image', 'image1', - '--flavor', 'flavor1', - '--nic', 'auto', - self.new_server.name, - ] + def _test_server_create_with_auto_network(self, arglist): verifylist = [ ('image', 'image1'), ('flavor', 'flavor1'), @@ -1946,6 +1940,27 @@ def test_server_create_with_auto_network(self): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist(), data) + # NOTE(stephenfin): '--auto-network' is an alias for '--nic auto' so the + # tests are nearly identical + + def test_server_create_with_auto_network_legacy(self): + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--nic', 'auto', + self.new_server.name, + ] + self._test_server_create_with_auto_network(arglist) + + def test_server_create_with_auto_network(self): + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--auto-network', + self.new_server.name, + ] + self._test_server_create_with_auto_network(arglist) + def test_server_create_with_auto_network_default_v2_37(self): """Tests creating a server without specifying --nic using 2.37.""" arglist = [ @@ -1996,13 +2011,7 @@ def test_server_create_with_auto_network_default_v2_37(self): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist(), data) - def test_server_create_with_none_network(self): - arglist = [ - '--image', 'image1', - '--flavor', 'flavor1', - '--nic', 'none', - self.new_server.name, - ] + def _test_server_create_with_none_network(self, arglist): verifylist = [ ('image', 'image1'), ('flavor', 'flavor1'), @@ -2042,6 +2051,27 @@ def test_server_create_with_none_network(self): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist(), data) + # NOTE(stephenfin): '--no-network' is an alias for '--nic none' so the + # tests are nearly identical + + def test_server_create_with_none_network_legacy(self): + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--nic', 'none', + self.new_server.name, + ] + self._test_server_create_with_none_network(arglist) + + def test_server_create_with_none_network(self): + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--no-network', + self.new_server.name, + ] + self._test_server_create_with_none_network(arglist) + def test_server_create_with_conflict_network_options(self): arglist = [ '--image', 'image1', diff --git a/releasenotes/notes/auto-no-network-options-f4ddb2bb7544d2f5.yaml b/releasenotes/notes/auto-no-network-options-f4ddb2bb7544d2f5.yaml new file mode 100644 index 0000000000..3123d775a7 --- /dev/null +++ b/releasenotes/notes/auto-no-network-options-f4ddb2bb7544d2f5.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + The ``server create`` command now accepts two new options, ``--no-network`` + and ``--auto-network``. These are aliases for ``--nic none`` and + ``--nic auto``, respectively. From d7aa53b9a2bb1969abee90b9d0a547230886aa0f Mon Sep 17 00:00:00 2001 From: melanie witt Date: Mon, 7 Nov 2022 22:54:55 +0000 Subject: [PATCH 046/595] Add note about microversion 2.87 in server rescue help The ability to rescue a volume-backed server was added in compute microversion 2.87 [1]. This adds a note to the command help to improve user experience. [1] https://docs.openstack.org/nova/latest/user/rescue.html Change-Id: I5f40c3ca28e13bd1f979bc5f8c337302a3b9a5be --- openstackclient/compute/v2/server.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 1d0c03a270..97084e5a38 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -3645,7 +3645,11 @@ def take_action(self, parsed_args): class RescueServer(command.Command): - _description = _("Put server in rescue mode") + _description = _( + "Put server in rescue mode. " + "Specify ``--os-compute-api-version 2.87`` or higher to rescue a " + "server booted from a volume." + ) def get_parser(self, prog_name): parser = super(RescueServer, self).get_parser(prog_name) From 1d71479a4cea2a90eb2f244864fbcc665ed9484f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 7 Nov 2022 16:35:56 +0000 Subject: [PATCH 047/595] zuul: Remove nova-network tests nova-network has been removed from nova for a very long time now and we've no way to test it in CI save for installing old versions of OpenStack. We don't care about this enough to do that, so just remove the thing. In the vein of things that have been removed, we also remove configuration that was supposed to enable cinder's v1 API but doesn't since the related knob was removed over 5 years ago [1]. [1] https://github.com/openstack/cinder/commit/3e91de956e1947a7014709010b99df380242ac74 Change-Id: I76efeccec04937c3a68108e2654872e00fadcec4 Signed-off-by: Stephen Finucane --- .zuul.yaml | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 7883a4b19c..95ab34aba2 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -54,12 +54,6 @@ LIBS_FROM_GIT: python-openstackclient # NOTE(dtroyer): Functional tests need a bit more volume headroom VOLUME_BACKING_FILE_SIZE: 20G - devstack_local_conf: - post-config: - $CINDER_CONF: - DEFAULT: - # NOTE(dtroyer): OSC needs to support Volume v1 for a while yet so re-enable - enable_v1_api: true devstack_services: ceilometer-acentral: false ceilometer-acompute: false @@ -114,28 +108,6 @@ Q_ML2_PLUGIN_MECHANISM_DRIVERS: openvswitch tox_envlist: functional -- job: - name: osc-functional-devstack-n-net - parent: osc-functional-devstack-base - timeout: 7800 - vars: - devstack_localrc: - FLAT_INTERFACE: br_flat - PUBLIC_INTERFACE: br_pub - devstack_services: - n-cell: true - n-net: true - neutron: false - neutron-segments: false - q-agt: false - q-dhcp: false - q-l3: false - q-meta: false - q-metering: false - q-qos: false - q-svc: false - tox_envlist: functional - - job: name: osc-functional-devstack-tips parent: osc-functional-devstack @@ -232,12 +204,6 @@ jobs: - osc-build-image - osc-functional-devstack - # - osc-functional-devstack-n-net: - # voting: false - # # The job testing nova-network no longer works before Pike, and - # # should be disabled until the New Way of testing against old clouds - # # is ready and backported - # branches: ^(?!stable/(newton|ocata)).*$ - osc-functional-devstack-tips: # The functional-tips job only tests the latest and shouldn't be run # on the stable branches From ed0d568b948a04e893270d297c538773d058b73e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Oct 2022 18:17:38 +0300 Subject: [PATCH 048/595] compute: Fix '--network none/auto' handling This should lookup a network called 'none' or 'auto', not do the equivalent on '--nic none' or '--nic auto'. Correct this. Change-Id: I3c5acc49bfe8162d8fb6110603da56d56090b78f Signed-off-by: Stephen Finucane Story: 2010385 Task: 46658 --- openstackclient/compute/v2/server.py | 11 +++-- .../tests/unit/compute/v2/test_server.py | 49 ++++++++++++++----- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 39b2bdc81e..1d07246364 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -721,11 +721,6 @@ def __call__(self, parser, namespace, values, option_string=None): if getattr(namespace, self.dest, None) is None: setattr(namespace, self.dest, []) - # Handle the special auto/none cases - if values in ('auto', 'none'): - getattr(namespace, self.dest).append(values) - return - if self.key: if ',' in values or '=' in values: msg = _( @@ -735,6 +730,12 @@ def __call__(self, parser, namespace, values, option_string=None): raise argparse.ArgumentTypeError(msg % values) values = '='.join([self.key, values]) + else: + # Handle the special auto/none cases but only when a key isn't set + # (otherwise those could be valid values for the key) + if values in ('auto', 'none'): + getattr(namespace, self.dest).append(values) + return # We don't include 'tag' here by default since that requires a # particular microversion diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 858c9a2a68..48a36172bf 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -1675,6 +1675,7 @@ def test_server_create_with_network(self): '--nic', 'net-id=net1,v4-fixed-ip=10.0.0.2', '--port', 'port1', '--network', 'net1', + '--network', 'auto', # this is a network called 'auto' '--nic', 'port-id=port2', self.new_server.name, ] @@ -1683,24 +1684,40 @@ def test_server_create_with_network(self): ('flavor', 'flavor1'), ('nics', [ { - 'net-id': 'net1', 'port-id': '', - 'v4-fixed-ip': '', 'v6-fixed-ip': '', + 'net-id': 'net1', + 'port-id': '', + 'v4-fixed-ip': '', + 'v6-fixed-ip': '', }, { - 'net-id': 'net1', 'port-id': '', - 'v4-fixed-ip': '10.0.0.2', 'v6-fixed-ip': '', + 'net-id': 'net1', + 'port-id': '', + 'v4-fixed-ip': '10.0.0.2', + 'v6-fixed-ip': '', }, { - 'net-id': '', 'port-id': 'port1', - 'v4-fixed-ip': '', 'v6-fixed-ip': '', + 'net-id': '', + 'port-id': 'port1', + 'v4-fixed-ip': '', + 'v6-fixed-ip': '', }, { - 'net-id': 'net1', 'port-id': '', - 'v4-fixed-ip': '', 'v6-fixed-ip': '', + 'net-id': 'net1', + 'port-id': '', + 'v4-fixed-ip': '', + 'v6-fixed-ip': '', }, { - 'net-id': '', 'port-id': 'port2', - 'v4-fixed-ip': '', 'v6-fixed-ip': '', + 'net-id': 'auto', + 'port-id': '', + 'v4-fixed-ip': '', + 'v6-fixed-ip': '', + }, + { + 'net-id': '', + 'port-id': 'port2', + 'v4-fixed-ip': '', + 'v6-fixed-ip': '', }, ]), ('config_drive', False), @@ -1729,12 +1746,16 @@ def test_server_create_with_network(self): "port2": port2_resource}[port_id]) # Mock sdk APIs. - _network = mock.Mock(id='net1_uuid') + _network_1 = mock.Mock(id='net1_uuid') + _network_auto = mock.Mock(id='auto_uuid') _port1 = mock.Mock(id='port1_uuid') _port2 = mock.Mock(id='port2_uuid') find_network = mock.Mock() find_port = mock.Mock() - find_network.return_value = _network + find_network.side_effect = lambda net_id, ignore_missing: { + "net1": _network_1, + "auto": _network_auto, + }[net_id] find_port.side_effect = (lambda port_id, ignore_missing: {"port1": _port1, "port2": _port2}[port_id]) @@ -1775,6 +1796,10 @@ def test_server_create_with_network(self): 'v4-fixed-ip': '', 'v6-fixed-ip': '', 'port-id': ''}, + {'net-id': 'auto_uuid', + 'v4-fixed-ip': '', + 'v6-fixed-ip': '', + 'port-id': ''}, {'net-id': '', 'v4-fixed-ip': '', 'v6-fixed-ip': '', From ffb69116b30cfbf7b981666528a3e417d502f93b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Oct 2022 18:18:50 +0300 Subject: [PATCH 049/595] compute: Add missing microversion check for networks The 'auto' and 'none' network allocation policies are only supported on compute API microversion 2.37 or later. Enforce this in the code. Change-Id: I90f8fb1e61ead4bd406ea76bbeb731b913805b13 Signed-off-by: Stephen Finucane Story: 2010385 Task: 46657 --- openstackclient/compute/v2/server.py | 8 ++ .../tests/unit/compute/v2/test_server.py | 100 +++++++++++++++--- 2 files changed, 91 insertions(+), 17 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 1d07246364..609faf5aad 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1615,6 +1615,14 @@ def _match_image(image_api, wanted_properties): ) raise exceptions.CommandError(msg) + if compute_client.api_version < api_versions.APIVersion('2.37'): + msg = _( + '--os-compute-api-version 2.37 or greater is ' + 'required to support explicit auto-allocation of a ' + 'network or to disable network allocation' + ) + raise exceptions.CommandError(msg) + nics = nics[0] else: for nic in nics: diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 48a36172bf..f59c954a3b 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -1926,6 +1926,10 @@ def test_server_create_with_network_tag_pre_v243(self): exceptions.CommandError, self.cmd.take_action, parsed_args) def _test_server_create_with_auto_network(self, arglist): + # requires API microversion 2.37 or later + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.37') + verifylist = [ ('image', 'image1'), ('flavor', 'flavor1'), @@ -1986,8 +1990,45 @@ def test_server_create_with_auto_network(self): ] self._test_server_create_with_auto_network(arglist) + def test_server_create_with_auto_network_pre_v237(self): + # use an API microversion that's too old + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.36') + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--nic', 'auto', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('nics', ['auto']), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + self.assertIn( + '--os-compute-api-version 2.37 or greater is required to support ' + 'explicit auto-allocation of a network or to disable network ' + 'allocation', + str(exc), + ) + self.assertNotCalled(self.servers_mock.create) + def test_server_create_with_auto_network_default_v2_37(self): """Tests creating a server without specifying --nic using 2.37.""" + # requires API microversion 2.37 or later + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.37') + arglist = [ '--image', 'image1', '--flavor', 'flavor1', @@ -2001,12 +2042,7 @@ def test_server_create_with_auto_network_default_v2_37(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # Since check_parser doesn't handle compute global options like - # --os-compute-api-version, we have to mock the construction of - # the novaclient client object with our own APIVersion. - with mock.patch.object(self.app.client_manager.compute, 'api_version', - api_versions.APIVersion('2.37')): - columns, data = self.cmd.take_action(parsed_args) + columns, data = self.cmd.take_action(parsed_args) # Set expected values kwargs = dict( @@ -2037,6 +2073,10 @@ def test_server_create_with_auto_network_default_v2_37(self): self.assertEqual(self.datalist(), data) def _test_server_create_with_none_network(self, arglist): + # requires API microversion 2.37 or later + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.37') + verifylist = [ ('image', 'image1'), ('flavor', 'flavor1'), @@ -2097,6 +2137,40 @@ def test_server_create_with_none_network(self): ] self._test_server_create_with_none_network(arglist) + def test_server_create_with_none_network_pre_v237(self): + # use an API microversion that's too old + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.36') + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--nic', 'none', + self.new_server.name, + ] + + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('nics', ['none']), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + self.assertIn( + '--os-compute-api-version 2.37 or greater is required to support ' + 'explicit auto-allocation of a network or to disable network ' + 'allocation', + str(exc), + ) + self.assertNotCalled(self.servers_mock.create) + def test_server_create_with_conflict_network_options(self): arglist = [ '--image', 'image1', @@ -3227,13 +3301,11 @@ def test_server_create_image_property(self): arglist = [ '--image-property', 'hypervisor_type=qemu', '--flavor', 'flavor1', - '--nic', 'none', self.new_server.name, ] verifylist = [ ('image_properties', {'hypervisor_type': 'qemu'}), ('flavor', 'flavor1'), - ('nics', ['none']), ('config_drive', False), ('server_name', self.new_server.name), ] @@ -3261,7 +3333,7 @@ def test_server_create_image_property(self): availability_zone=None, admin_pass=None, block_device_mapping_v2=[], - nics='none', + nics=[], meta=None, scheduler_hints={}, config_drive=None, @@ -3282,14 +3354,12 @@ def test_server_create_image_property_multi(self): '--image-property', 'hypervisor_type=qemu', '--image-property', 'hw_disk_bus=ide', '--flavor', 'flavor1', - '--nic', 'none', self.new_server.name, ] verifylist = [ ('image_properties', {'hypervisor_type': 'qemu', 'hw_disk_bus': 'ide'}), ('flavor', 'flavor1'), - ('nics', ['none']), ('config_drive', False), ('server_name', self.new_server.name), ] @@ -3317,7 +3387,7 @@ def test_server_create_image_property_multi(self): availability_zone=None, admin_pass=None, block_device_mapping_v2=[], - nics='none', + nics=[], meta=None, scheduler_hints={}, config_drive=None, @@ -3338,14 +3408,12 @@ def test_server_create_image_property_missed(self): '--image-property', 'hypervisor_type=qemu', '--image-property', 'hw_disk_bus=virtio', '--flavor', 'flavor1', - '--nic', 'none', self.new_server.name, ] verifylist = [ ('image_properties', {'hypervisor_type': 'qemu', 'hw_disk_bus': 'virtio'}), ('flavor', 'flavor1'), - ('nics', ['none']), ('config_drive', False), ('server_name', self.new_server.name), ] @@ -3369,7 +3437,6 @@ def test_server_create_image_property_with_image_list(self): '--image-property', 'owner_specified.openstack.object=image/cirros', '--flavor', 'flavor1', - '--nic', 'none', self.new_server.name, ] @@ -3377,7 +3444,6 @@ def test_server_create_image_property_with_image_list(self): ('image_properties', {'owner_specified.openstack.object': 'image/cirros'}), ('flavor', 'flavor1'), - ('nics', ['none']), ('server_name', self.new_server.name), ] # create a image_info as the side_effect of the fake image_list() @@ -3407,7 +3473,7 @@ def test_server_create_image_property_with_image_list(self): availability_zone=None, admin_pass=None, block_device_mapping_v2=[], - nics='none', + nics=[], meta=None, scheduler_hints={}, config_drive=None, From a244bb84e07617dad12dee91bbe63bdca3357b1e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 7 Nov 2022 17:02:58 +0000 Subject: [PATCH 050/595] tests: Move json decoding to base test class We do this everywhere. Add a simple knob to simplify the pattern. Only one use is migrated initially. The rest will be done separately. Change-Id: Ic3b8958bd4fb1459a8ac3adaff216c2a26628491 Signed-off-by: Stephen Finucane --- openstackclient/tests/functional/base.py | 62 ++++++++++++------- .../tests/functional/common/test_module.py | 16 +++-- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/openstackclient/tests/functional/base.py b/openstackclient/tests/functional/base.py index b6867f819d..0c430267be 100644 --- a/openstackclient/tests/functional/base.py +++ b/openstackclient/tests/functional/base.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import json import logging import os import shlex @@ -48,33 +49,52 @@ def execute(cmd, fail_ok=False, merge_stderr=False): class TestCase(testtools.TestCase): @classmethod - def openstack(cls, cmd, cloud=ADMIN_CLOUD, fail_ok=False): + def openstack( + cls, + cmd, + *, + cloud=ADMIN_CLOUD, + fail_ok=False, + parse_output=False, + ): """Executes openstackclient command for the given action - NOTE(dtroyer): There is a subtle distinction between passing - cloud=None and cloud='': for compatibility reasons passing - cloud=None continues to include the option '--os-auth-type none' - in the command while passing cloud='' omits the '--os-auth-type' - option completely to let the default handlers be invoked. + :param cmd: A string representation of the command to execute. + :param cloud: The cloud to execute against. This can be a string, empty + string, or None. A string results in '--os-auth-type $cloud', an + empty string results in the '--os-auth-type' option being + omitted, and None resuts in '--os-auth-type none' for legacy + reasons. + :param fail_ok: If failure is permitted. If False (default), a command + failure will result in `~tempest.lib.exceptions.CommandFailed` + being raised. + :param parse_output: If true, pass the '-f json' parameter and decode + the output. + :returns: The output from the command. + :raises: `~tempest.lib.exceptions.CommandFailed` if the command failed + and ``fail_ok`` was ``False``. """ + auth_args = [] if cloud is None: # Execute command with no auth - return execute( - 'openstack --os-auth-type none ' + cmd, - fail_ok=fail_ok - ) - elif cloud == '': - # Execute command with no auth options at all - return execute( - 'openstack ' + cmd, - fail_ok=fail_ok - ) - else: + auth_args.append('--os-auth-type none') + elif cloud != '': # Execute command with an explicit cloud specified - return execute( - 'openstack --os-cloud=' + cloud + ' ' + cmd, - fail_ok=fail_ok - ) + auth_args.append(f'--os-cloud {cloud}') + + format_args = [] + if parse_output: + format_args.append('-f json') + + output = execute( + ' '.join(['openstack'] + auth_args + [cmd] + format_args), + fail_ok=fail_ok, + ) + + if parse_output: + return json.loads(output) + else: + return output @classmethod def is_service_enabled(cls, service, version=None): diff --git a/openstackclient/tests/functional/common/test_module.py b/openstackclient/tests/functional/common/test_module.py index 41aabb7f85..967d3b4982 100644 --- a/openstackclient/tests/functional/common/test_module.py +++ b/openstackclient/tests/functional/common/test_module.py @@ -11,9 +11,6 @@ # 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 @@ -31,14 +28,14 @@ class ModuleTest(base.TestCase): def test_module_list(self): # Test module list - cmd_output = json.loads(self.openstack('module list -f json')) + cmd_output = self.openstack('module list', parse_output=True) for one_module in self.CLIENTS: self.assertIn(one_module, cmd_output.keys()) for one_module in self.LIBS: self.assertNotIn(one_module, cmd_output.keys()) # Test module list --all - cmd_output = json.loads(self.openstack('module list --all -f json')) + cmd_output = self.openstack('module list --all', parse_output=True) for one_module in self.CLIENTS + self.LIBS: self.assertIn(one_module, cmd_output.keys()) @@ -56,7 +53,7 @@ class CommandTest(base.TestCase): ] def test_command_list_no_option(self): - cmd_output = json.loads(self.openstack('command list -f json')) + cmd_output = self.openstack('command list', parse_output=True) group_names = [each.get('Command Group') for each in cmd_output] for one_group in self.GROUPS: self.assertIn(one_group, group_names) @@ -70,9 +67,10 @@ def test_command_list_with_group(self): 'compute.v2' ] for each_input in input_groups: - cmd_output = json.loads(self.openstack( - 'command list --group %s -f json' % each_input - )) + cmd_output = self.openstack( + 'command list --group %s' % each_input, + parse_output=True, + ) group_names = [each.get('Command Group') for each in cmd_output] for each_name in group_names: self.assertIn(each_input, each_name) From 38e39b6dc14fd88318541728cb34fd8442d59e8a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 7 Nov 2022 17:21:12 +0000 Subject: [PATCH 051/595] tests: Convert more functional tests to use 'parse_output' Change-Id: I1d968181eb196c6df4583c772c67ed58bc7ba585 Signed-off-by: Stephen Finucane --- .../tests/functional/common/test_args.py | 25 ++--- .../common/test_availability_zone.py | 8 +- .../functional/common/test_configuration.py | 24 +++-- .../tests/functional/common/test_extension.py | 56 ++++------- .../tests/functional/common/test_quota.py | 99 +++++++++++-------- .../tests/functional/common/test_versions.py | 6 +- 6 files changed, 105 insertions(+), 113 deletions(-) diff --git a/openstackclient/tests/functional/common/test_args.py b/openstackclient/tests/functional/common/test_args.py index 02cad6c16a..1f5ecc1cfe 100644 --- a/openstackclient/tests/functional/common/test_args.py +++ b/openstackclient/tests/functional/common/test_args.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import json - from tempest.lib import exceptions as tempest_exc from openstackclient.tests.functional import base @@ -21,10 +19,11 @@ class ArgumentTests(base.TestCase): """Functional tests for command line arguments""" def test_default_auth_type(self): - cmd_output = json.loads(self.openstack( - 'configuration show -f json', + cmd_output = self.openstack( + 'configuration show', cloud='', - )) + parse_output=True, + ) self.assertIsNotNone(cmd_output) self.assertIn( 'auth_type', @@ -36,10 +35,11 @@ def test_default_auth_type(self): ) def test_auth_type_none(self): - cmd_output = json.loads(self.openstack( - 'configuration show -f json', + cmd_output = self.openstack( + 'configuration show', cloud=None, - )) + parse_output=True, + ) self.assertIsNotNone(cmd_output) self.assertIn( 'auth_type', @@ -54,7 +54,7 @@ def test_auth_type_token_endpoint_opt(self): # Make sure token_endpoint is really gone try: self.openstack( - 'configuration show -f json --os-auth-type token_endpoint', + 'configuration show --os-auth-type token_endpoint', cloud=None, ) except tempest_exc.CommandFailed as e: @@ -64,10 +64,11 @@ def test_auth_type_token_endpoint_opt(self): self.fail('CommandFailed should be raised') def test_auth_type_password_opt(self): - cmd_output = json.loads(self.openstack( - 'configuration show -f json --os-auth-type password', + cmd_output = self.openstack( + 'configuration show --os-auth-type password', cloud=None, - )) + parse_output=True, + ) self.assertIsNotNone(cmd_output) self.assertIn( 'auth_type', diff --git a/openstackclient/tests/functional/common/test_availability_zone.py b/openstackclient/tests/functional/common/test_availability_zone.py index 025da95c24..f319ffc5ef 100644 --- a/openstackclient/tests/functional/common/test_availability_zone.py +++ b/openstackclient/tests/functional/common/test_availability_zone.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import json - from openstackclient.tests.functional import base @@ -19,8 +17,10 @@ class AvailabilityZoneTests(base.TestCase): """Functional tests for availability zone. """ def test_availability_zone_list(self): - cmd_output = json.loads(self.openstack( - 'availability zone list -f json')) + cmd_output = self.openstack( + 'availability zone list', + parse_output=True, + ) zones = [x['Zone Name'] for x in cmd_output] self.assertIn( 'internal', diff --git a/openstackclient/tests/functional/common/test_configuration.py b/openstackclient/tests/functional/common/test_configuration.py index 17e0f45d1f..614b3e46ee 100644 --- a/openstackclient/tests/functional/common/test_configuration.py +++ b/openstackclient/tests/functional/common/test_configuration.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import json import os from openstackclient.common import configuration @@ -30,9 +29,7 @@ def test_configuration_show(self): items = self.parse_listing(raw_output) self.assert_table_structure(items, BASIC_CONFIG_HEADERS) - cmd_output = json.loads(self.openstack( - 'configuration show -f json' - )) + cmd_output = self.openstack('configuration show', parse_output=True) self.assertEqual( configuration.REDACTED, cmd_output['auth.password'] @@ -43,18 +40,18 @@ def test_configuration_show(self): ) # Test show --mask - cmd_output = json.loads(self.openstack( - 'configuration show --mask -f json' - )) + cmd_output = self.openstack( + 'configuration show --mask', parse_output=True, + ) self.assertEqual( configuration.REDACTED, cmd_output['auth.password'] ) # Test show --unmask - cmd_output = json.loads(self.openstack( - 'configuration show --unmask -f json' - )) + cmd_output = self.openstack( + 'configuration show --unmask', parse_output=True, + ) # If we are using os-client-config, this will not be set. Rather than # parse clouds.yaml to get the right value, just make sure # we are not getting redacted. @@ -84,10 +81,11 @@ def test_configuration_show(self): items = self.parse_listing(raw_output) self.assert_table_structure(items, BASIC_CONFIG_HEADERS) - cmd_output = json.loads(self.openstack( - 'configuration show -f json', + cmd_output = self.openstack( + 'configuration show', cloud=None, - )) + parse_output=True, + ) self.assertNotIn( 'auth.password', cmd_output, diff --git a/openstackclient/tests/functional/common/test_extension.py b/openstackclient/tests/functional/common/test_extension.py index 92efabefe9..8784c55b14 100644 --- a/openstackclient/tests/functional/common/test_extension.py +++ b/openstackclient/tests/functional/common/test_extension.py @@ -13,8 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -import json - from tempest.lib import exceptions as tempest_exc from openstackclient.tests.functional import base @@ -30,11 +28,11 @@ def setUpClass(cls): def test_extension_list_compute(self): """Test compute extension list""" - json_output = json.loads(self.openstack( - 'extension list -f json ' + - '--compute' - )) - name_list = [item.get('Name') for item in json_output] + output = self.openstack( + 'extension list --compute', + parse_output=True, + ) + name_list = [item.get('Name') for item in output] self.assertIn( 'ImageSize', name_list, @@ -42,11 +40,11 @@ def test_extension_list_compute(self): def test_extension_list_volume(self): """Test volume extension list""" - json_output = json.loads(self.openstack( - 'extension list -f json ' + - '--volume' - )) - name_list = [item.get('Name') for item in json_output] + output = self.openstack( + 'extension list --volume', + parse_output=True, + ) + name_list = [item.get('Name') for item in output] self.assertIn( 'TypesManage', name_list, @@ -57,43 +55,29 @@ def test_extension_list_network(self): if not self.haz_network: self.skipTest("No Network service present") - json_output = json.loads(self.openstack( - 'extension list -f json ' + - '--network' - )) - name_list = [item.get('Name') for item in json_output] + output = self.openstack( + 'extension list --network', + parse_output=True, + ) + name_list = [item.get('Name') for item in output] self.assertIn( 'Default Subnetpools', name_list, ) - # NOTE(dtroyer): Only network extensions are currently supported but - # I am going to leave this here anyway as a reminder - # fix that. - # def test_extension_show_compute(self): - # """Test compute extension show""" - # json_output = json.loads(self.openstack( - # 'extension show -f json ' + - # 'ImageSize' - # )) - # self.assertEqual( - # 'OS-EXT-IMG-SIZE', - # json_output.get('Alias'), - # ) - def test_extension_show_network(self): """Test network extension show""" if not self.haz_network: self.skipTest("No Network service present") name = 'agent' - json_output = json.loads(self.openstack( - 'extension show -f json ' + - name - )) + output = self.openstack( + 'extension show ' + name, + parse_output=True, + ) self.assertEqual( name, - json_output.get('alias'), + output.get('alias'), ) def test_extension_show_not_exist(self): diff --git a/openstackclient/tests/functional/common/test_quota.py b/openstackclient/tests/functional/common/test_quota.py index 9089cba5c0..6e48df1d76 100644 --- a/openstackclient/tests/functional/common/test_quota.py +++ b/openstackclient/tests/functional/common/test_quota.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import json import uuid from tempest.lib import exceptions @@ -36,9 +35,10 @@ def setUpClass(cls): def test_quota_list_details_compute(self): expected_headers = ["Resource", "In Use", "Reserved", "Limit"] - cmd_output = json.loads(self.openstack( - 'quota list -f json --detail --compute' - )) + cmd_output = self.openstack( + 'quota list --detail --compute', + parse_output=True, + ) self.assertIsNotNone(cmd_output) resources = [] for row in cmd_output: @@ -52,9 +52,10 @@ def test_quota_list_details_compute(self): def test_quota_list_details_network(self): expected_headers = ["Resource", "In Use", "Reserved", "Limit"] - cmd_output = json.loads(self.openstack( - 'quota list -f json --detail --network' - )) + cmd_output = self.openstack( + 'quota list --detail --network', + parse_output=True, + ) self.assertIsNotNone(cmd_output) resources = [] for row in cmd_output: @@ -70,9 +71,10 @@ def test_quota_list_network_option(self): if not self.haz_network: self.skipTest("No Network service present") self.openstack('quota set --networks 40 ' + self.PROJECT_NAME) - cmd_output = json.loads(self.openstack( - 'quota list -f json --network' - )) + cmd_output = self.openstack( + 'quota list --network', + parse_output=True, + ) self.assertIsNotNone(cmd_output) self.assertEqual( 40, @@ -81,9 +83,10 @@ def test_quota_list_network_option(self): def test_quota_list_compute_option(self): self.openstack('quota set --instances 30 ' + self.PROJECT_NAME) - cmd_output = json.loads(self.openstack( - 'quota list -f json --compute' - )) + cmd_output = self.openstack( + 'quota list --compute', + parse_output=True, + ) self.assertIsNotNone(cmd_output) self.assertEqual( 30, @@ -92,9 +95,10 @@ def test_quota_list_compute_option(self): def test_quota_list_volume_option(self): self.openstack('quota set --volumes 20 ' + self.PROJECT_NAME) - cmd_output = json.loads(self.openstack( - 'quota list -f json --volume' - )) + cmd_output = self.openstack( + 'quota list --volume', + parse_output=True, + ) self.assertIsNotNone(cmd_output) self.assertEqual( 20, @@ -111,9 +115,10 @@ def test_quota_set_project(self): network_option + self.PROJECT_NAME ) - cmd_output = json.loads(self.openstack( - 'quota show -f json ' + self.PROJECT_NAME - )) + cmd_output = self.openstack( + 'quota show ' + self.PROJECT_NAME, + parse_output=True, + ) cmd_output = {x['Resource']: x['Limit'] for x in cmd_output} self.assertIsNotNone(cmd_output) self.assertEqual( @@ -131,9 +136,10 @@ def test_quota_set_project(self): ) # Check default quotas - cmd_output = json.loads(self.openstack( - 'quota show -f json --default' - )) + cmd_output = self.openstack( + 'quota show --default', + parse_output=True, + ) self.assertIsNotNone(cmd_output) # We don't necessarily know the default quotas, we're checking the # returned attributes @@ -148,9 +154,10 @@ def test_quota_set_class(self): 'quota set --key-pairs 33 --snapshots 43 ' + '--class default' ) - cmd_output = json.loads(self.openstack( - 'quota show -f json --class default' - )) + cmd_output = self.openstack( + 'quota show --class default', + parse_output=True, + ) self.assertIsNotNone(cmd_output) cmd_output = {x['Resource']: x['Limit'] for x in cmd_output} self.assertEqual( @@ -163,9 +170,10 @@ def test_quota_set_class(self): ) # Check default quota class - cmd_output = json.loads(self.openstack( - 'quota show -f json --class' - )) + cmd_output = self.openstack( + 'quota show --class', + parse_output=True, + ) self.assertIsNotNone(cmd_output) # We don't necessarily know the default quotas, we're checking the # returned attributes @@ -182,16 +190,18 @@ def test_quota_network_set_with_no_force(self): if not self.is_extension_enabled('quota-check-limit'): self.skipTest('No "quota-check-limit" extension present') - cmd_output = json.loads(self.openstack( - 'quota list -f json --network' - )) + cmd_output = self.openstack( + 'quota list --network', + parse_output=True, + ) self.addCleanup(self._restore_quota_limit, 'network', cmd_output[0]['Networks'], self.PROJECT_NAME) self.openstack('quota set --networks 40 ' + self.PROJECT_NAME) - cmd_output = json.loads(self.openstack( - 'quota list -f json --network' - )) + cmd_output = self.openstack( + 'quota list --network', + parse_output=True, + ) self.assertIsNotNone(cmd_output) self.assertEqual(40, cmd_output[0]['Networks']) @@ -218,16 +228,18 @@ def test_quota_network_set_with_force(self): if not self.is_extension_enabled('quota-check-limit'): self.skipTest('No "quota-check-limit" extension present') - cmd_output = json.loads(self.openstack( - 'quota list -f json --network' - )) + cmd_output = self.openstack( + 'quota list --network', + parse_output=True, + ) self.addCleanup(self._restore_quota_limit, 'network', cmd_output[0]['Networks'], self.PROJECT_NAME) self.openstack('quota set --networks 40 ' + self.PROJECT_NAME) - cmd_output = json.loads(self.openstack( - 'quota list -f json --network' - )) + cmd_output = self.openstack( + 'quota list --network', + parse_output=True, + ) self.assertIsNotNone(cmd_output) self.assertEqual(40, cmd_output[0]['Networks']) @@ -237,8 +249,9 @@ def test_quota_network_set_with_force(self): (self.PROJECT_NAME, uuid.uuid4().hex)) self.openstack('quota set --networks 1 --force ' + self.PROJECT_NAME) - cmd_output = json.loads(self.openstack( - 'quota list -f json --network' - )) + cmd_output = self.openstack( + 'quota list --network', + parse_output=True, + ) self.assertIsNotNone(cmd_output) self.assertEqual(1, cmd_output[0]['Networks']) diff --git a/openstackclient/tests/functional/common/test_versions.py b/openstackclient/tests/functional/common/test_versions.py index adc74ebc6c..6575671aa1 100644 --- a/openstackclient/tests/functional/common/test_versions.py +++ b/openstackclient/tests/functional/common/test_versions.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import json - from openstackclient.tests.functional import base @@ -21,9 +19,7 @@ class VersionsTests(base.TestCase): def test_versions_show(self): # TODO(mordred) Make this better. The trick is knowing what in the # payload to test for. - cmd_output = json.loads(self.openstack( - 'versions show -f json' - )) + cmd_output = self.openstack('versions show', parse_output=True) self.assertIsNotNone(cmd_output) self.assertIn( "Region Name", From b7d01833d02260006e6d4c6e0a065748ecc8b913 Mon Sep 17 00:00:00 2001 From: Maksim Malchuk Date: Wed, 9 Nov 2022 17:28:28 +0300 Subject: [PATCH 052/595] Add baremetal agent type list filtering This change adds and ability to filter out the baremetal nodes in 'network agent list' command. Related-Story: 2008590 Related-Task: 41746 Related-Bug: #1658964 Change-Id: I01ffbd82662abbc1c2f56eb8f1e700f392bc063c Signed-off-by: Maksim Malchuk --- openstackclient/network/v2/network_agent.py | 7 ++++--- .../notes/add-baremetal-agent-type-7c46365e8d457ac8.yaml | 5 +++++ 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/add-baremetal-agent-type-7c46365e8d457ac8.yaml diff --git a/openstackclient/network/v2/network_agent.py b/openstackclient/network/v2/network_agent.py index d963b3bf33..f67f67bd6c 100644 --- a/openstackclient/network/v2/network_agent.py +++ b/openstackclient/network/v2/network_agent.py @@ -168,11 +168,11 @@ def get_parser(self, prog_name): metavar='', 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 @@ def take_action(self, parsed_args): 'metering': 'Metering agent', 'metadata': 'Metadata agent', 'macvtap': 'Macvtap agent', - 'nic': 'NIC Switch agent' + 'nic': 'NIC Switch agent', + 'baremetal': 'Baremetal Node' } filters = {} 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 0000000000..a9a3a0dfd4 --- /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. From bafece762a5d0b03e28f9d81c98ad46777f56a34 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 21 Oct 2022 16:40:36 +0100 Subject: [PATCH 053/595] image: Ignore '--progress' if providing image data from stdin You can provide data via stdin when creating an image. Using this with '--progress' makes no sense and causes an error currently. Fix this. Change-Id: I3c2d658b72a7c62931b779b0d19bb97f60a0c655 Signed-off-by: Stephen Finucane --- openstackclient/image/v2/image.py | 4 ++- .../tests/unit/image/v2/test_image.py | 31 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 2342fd3e25..1ff8ad3ae9 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -476,7 +476,9 @@ def _take_action_image(self, parsed_args): LOG.warning(_("Failed to get an image file.")) return {}, {} - if fp is not None and parsed_args.progress: + if parsed_args.progress and parsed_args.file: + # NOTE(stephenfin): we only show a progress bar if the user + # requested it *and* we're reading from a file (not stdin) filesize = os.path.getsize(fname) if filesize is not None: kwargs['validate_checksum'] = False diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index f2c113649c..e17363a500 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -252,6 +252,37 @@ def test_image_create_file(self): self.expected_data, data) + @mock.patch('openstackclient.image.v2.image.get_data_file') + def test_image_create__progress_ignore_with_stdin( + self, mock_get_data_file, + ): + fake_stdin = io.StringIO('fake-image-data') + mock_get_data_file.return_value = (fake_stdin, None) + + arglist = [ + '--progress', + self.new_image.name, + ] + verifylist = [ + ('progress', True), + ('name', self.new_image.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + 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, + data=fake_stdin, + validate_checksum=False, + ) + + self.assertEqual(self.expected_columns, columns) + self.assertCountEqual(self.expected_data, data) + def test_image_create_dead_options(self): arglist = [ From 3d9a9df935af1f76a33d008fe76975475c77a268 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 21 Oct 2022 16:40:55 +0100 Subject: [PATCH 054/595] image: Simplify handling of data provided via stdin This was unnecessarily complex. Change-Id: I8289d5ce7356d8bc89425590a7f71bca91a6d396 Signed-off-by: Stephen Finucane --- openstackclient/image/v2/image.py | 91 ++++++++++-------- .../tests/unit/image/v2/test_image.py | 94 ++++++------------- 2 files changed, 82 insertions(+), 103 deletions(-) diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 1ff8ad3ae9..53cfadede0 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -134,34 +134,32 @@ def _get_member_columns(item): ) -def get_data_file(args): - if args.file: - return (open(args.file, 'rb'), args.file) +def get_data_from_stdin(): + # distinguish cases where: + # (1) stdin is not valid (as in cron jobs): + # openstack ... <&- + # (2) image data is provided through stdin: + # openstack ... < /tmp/file + # (3) no image data provided + # openstack ... + try: + os.fstat(0) + except OSError: + # (1) stdin is not valid + return None + + if not sys.stdin.isatty(): + # (2) image data is provided through stdin + image = sys.stdin + if hasattr(sys.stdin, 'buffer'): + image = sys.stdin.buffer + if msvcrt: + msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) + + return image else: - # distinguish cases where: - # (1) stdin is not valid (as in cron jobs): - # openstack ... <&- - # (2) image data is provided through stdin: - # openstack ... < /tmp/file - # (3) no image data provided - # openstack ... - try: - os.fstat(0) - except OSError: - # (1) stdin is not valid - return (None, None) - if not sys.stdin.isatty(): - # (2) image data is provided through stdin - image = sys.stdin - if hasattr(sys.stdin, 'buffer'): - image = sys.stdin.buffer - if msvcrt: - msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) - - return (image, None) - else: - # (3) - return (None, None) + # (3) + return None class AddProjectToImage(command.ShowOne): @@ -277,6 +275,7 @@ def get_parser(self, prog_name): source_group = parser.add_mutually_exclusive_group() source_group.add_argument( "--file", + dest="filename", metavar="", help=_("Upload image from local file"), ) @@ -299,7 +298,10 @@ def get_parser(self, prog_name): "--progress", action="store_true", default=False, - help=_("Show upload progress bar."), + help=_( + "Show upload progress bar " + "(ignored if passing data via stdin)" + ), ) parser.add_argument( '--sign-key-path', @@ -463,7 +465,15 @@ def _take_action_image(self, parsed_args): # open the file first to ensure any failures are handled before the # image is created. Get the file name (if it is file, and not stdin) # for easier further handling. - fp, fname = get_data_file(parsed_args) + if parsed_args.filename: + try: + fp = open(parsed_args.filename, 'rb') + except FileNotFoundError: + raise exceptions.CommandError( + '%r is not a valid file' % parsed_args.filename, + ) + else: + fp = get_data_from_stdin() if fp is not None and parsed_args.volume: msg = _( @@ -472,26 +482,24 @@ def _take_action_image(self, parsed_args): ) raise exceptions.CommandError(msg) - if fp is None and parsed_args.file: - LOG.warning(_("Failed to get an image file.")) - return {}, {} - - if parsed_args.progress and parsed_args.file: + if parsed_args.progress and parsed_args.filename: # NOTE(stephenfin): we only show a progress bar if the user # requested it *and* we're reading from a file (not stdin) - filesize = os.path.getsize(fname) + filesize = os.path.getsize(parsed_args.filename) if filesize is not None: kwargs['validate_checksum'] = False kwargs['data'] = progressbar.VerboseFileWrapper(fp, filesize) - elif fname: - kwargs['filename'] = fname + else: + kwargs['data'] = fp + elif parsed_args.filename: + kwargs['filename'] = parsed_args.filename elif fp: kwargs['validate_checksum'] = False kwargs['data'] = fp # sign an image using a given local private key file if parsed_args.sign_key_path or parsed_args.sign_cert_id: - if not parsed_args.file: + if not parsed_args.filename: msg = _( "signing an image requires the --file option, " "passing files via stdin when signing is not " @@ -546,6 +554,10 @@ def _take_action_image(self, parsed_args): kwargs['img_signature_key_type'] = signer.padding_method image = image_client.create_image(**kwargs) + + if parsed_args.filename: + fp.close() + return _format_image(image) def _take_action_volume(self, parsed_args): @@ -980,6 +992,7 @@ def get_parser(self, prog_name): parser.add_argument( "--file", metavar="", + dest="filename", help=_("Downloaded image save filename (default: stdout)"), ) parser.add_argument( @@ -993,7 +1006,7 @@ def take_action(self, parsed_args): image_client = self.app.client_manager.image image = image_client.find_image(parsed_args.image) - output_file = parsed_args.file + output_file = parsed_args.filename if output_file is None: output_file = getattr(sys.stdout, "buffer", sys.stdout) diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index e17363a500..ac9ddae6e9 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -14,7 +14,6 @@ import copy import io -import os import tempfile from unittest import mock @@ -104,15 +103,8 @@ def test_image_reserve_no_options(self, raw_input): disk_format=image.DEFAULT_DISK_FORMAT, ) - # Verify update() was not called, if it was show the args - self.assertEqual(self.client.update_image.call_args_list, []) - - self.assertEqual( - self.expected_columns, - columns) - self.assertCountEqual( - self.expected_data, - data) + self.assertEqual(self.expected_columns, columns) + self.assertCountEqual(self.expected_data, data) @mock.patch('sys.stdin', side_effect=[None]) def test_image_reserve_options(self, raw_input): @@ -121,10 +113,11 @@ def test_image_reserve_options(self, raw_input): '--disk-format', 'ami', '--min-disk', '10', '--min-ram', '4', - ('--protected' - if self.new_image.is_protected else '--unprotected'), - ('--private' - if self.new_image.visibility == 'private' else '--public'), + '--protected' if self.new_image.is_protected else '--unprotected', + ( + '--private' + if self.new_image.visibility == 'private' else '--public' + ), '--project', self.new_image.owner_id, '--project-domain', self.domain.id, self.new_image.name, @@ -160,12 +153,8 @@ def test_image_reserve_options(self, raw_input): visibility=self.new_image.visibility, ) - self.assertEqual( - self.expected_columns, - columns) - self.assertCountEqual( - self.expected_data, - data) + self.assertEqual(self.expected_columns, columns) + self.assertCountEqual(self.expected_data, data) def test_image_create_with_unexist_project(self): self.project_mock.get.side_effect = exceptions.NotFound(None) @@ -217,7 +206,7 @@ def test_image_create_file(self): self.new_image.name, ] verifylist = [ - ('file', imagefile.name), + ('filename', imagefile.name), ('is_protected', self.new_image.is_protected), ('visibility', self.new_image.visibility), ('properties', {'Alpha': '1', 'Beta': '2'}), @@ -252,12 +241,12 @@ def test_image_create_file(self): self.expected_data, data) - @mock.patch('openstackclient.image.v2.image.get_data_file') + @mock.patch('openstackclient.image.v2.image.get_data_from_stdin') def test_image_create__progress_ignore_with_stdin( - self, mock_get_data_file, + self, mock_get_data_from_stdin, ): fake_stdin = io.StringIO('fake-image-data') - mock_get_data_file.return_value = (fake_stdin, None) + mock_get_data_from_stdin.return_value = fake_stdin arglist = [ '--progress', @@ -322,11 +311,11 @@ def test_image_create_import(self, raw_input): ) @mock.patch('osc_lib.utils.find_resource') - @mock.patch('openstackclient.image.v2.image.get_data_file') + @mock.patch('openstackclient.image.v2.image.get_data_from_stdin') def test_image_create_from_volume(self, mock_get_data_f, mock_get_vol): fake_vol_id = 'fake-volume-id' - mock_get_data_f.return_value = (None, None) + mock_get_data_f.return_value = None class FakeVolume: id = fake_vol_id @@ -353,12 +342,12 @@ class FakeVolume: ) @mock.patch('osc_lib.utils.find_resource') - @mock.patch('openstackclient.image.v2.image.get_data_file') + @mock.patch('openstackclient.image.v2.image.get_data_from_stdin') def test_image_create_from_volume_fail(self, mock_get_data_f, mock_get_vol): fake_vol_id = 'fake-volume-id' - mock_get_data_f.return_value = (None, None) + mock_get_data_f.return_value = None class FakeVolume: id = fake_vol_id @@ -379,7 +368,7 @@ class FakeVolume: parsed_args) @mock.patch('osc_lib.utils.find_resource') - @mock.patch('openstackclient.image.v2.image.get_data_file') + @mock.patch('openstackclient.image.v2.image.get_data_from_stdin') def test_image_create_from_volume_v31(self, mock_get_data_f, mock_get_vol): @@ -387,7 +376,7 @@ def test_image_create_from_volume_v31(self, mock_get_data_f, api_versions.APIVersion('3.1')) fake_vol_id = 'fake-volume-id' - mock_get_data_f.return_value = (None, None) + mock_get_data_f.return_value = None class FakeVolume: id = fake_vol_id @@ -1798,7 +1787,7 @@ def test_save_data(self): arglist = ['--file', '/path/to/file', self.image.id] verifylist = [ - ('file', '/path/to/file'), + ('filename', '/path/to/file'), ('image', self.image.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1813,49 +1802,26 @@ def test_save_data(self): class TestImageGetData(TestImage): - def setUp(self): - super().setUp() - self.args = mock.Mock() - - def test_get_data_file_file(self): - (fd, fname) = tempfile.mkstemp(prefix='osc_test_image') - self.args.file = fname - - (test_fd, test_name) = image.get_data_file(self.args) - - self.assertEqual(fname, test_name) - test_fd.close() - - os.unlink(fname) - - def test_get_data_file_2(self): - - self.args.file = None - - f = io.BytesIO(b"some initial binary data: \x00\x01") + def test_get_data_from_stdin(self): + fd = io.BytesIO(b"some initial binary data: \x00\x01") with mock.patch('sys.stdin') as stdin: - stdin.return_value = f + stdin.return_value = fd stdin.isatty.return_value = False - stdin.buffer = f + stdin.buffer = fd - (test_fd, test_name) = image.get_data_file(self.args) + test_fd = image.get_data_from_stdin() # Ensure data written to temp file is correct - self.assertEqual(f, test_fd) - self.assertIsNone(test_name) - - def test_get_data_file_3(self): - - self.args.file = None + self.assertEqual(fd, test_fd) - f = io.BytesIO(b"some initial binary data: \x00\x01") + def test_get_data_from_stdin__interactive(self): + fd = io.BytesIO(b"some initial binary data: \x00\x01") with mock.patch('sys.stdin') as stdin: # There is stdin, but interactive - stdin.return_value = f + stdin.return_value = fd - (test_fd, test_fname) = image.get_data_file(self.args) + test_fd = image.get_data_from_stdin() self.assertIsNone(test_fd) - self.assertIsNone(test_fname) From 1fb8d1f48b256a2bad78e7d5633ea53c6537907c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 19 Oct 2022 18:07:41 +0100 Subject: [PATCH 055/595] image: Add 'image stage' command This is the equivalent of the 'image-stage' glanceclient command. Change-Id: I10b01ef145740a2f7ffe5a8c7ce0296df0ece0bd Signed-off-by: Stephen Finucane --- doc/source/cli/data/glance.csv | 2 +- openstackclient/image/v2/image.py | 77 +++++++++++++ openstackclient/tests/unit/image/v2/fakes.py | 1 + .../tests/unit/image/v2/test_image.py | 104 +++++++++++++----- .../notes/image-stage-ac19c47e6a52ffeb.yaml | 5 + setup.cfg | 1 + 6 files changed, 164 insertions(+), 26 deletions(-) create mode 100644 releasenotes/notes/image-stage-ac19c47e6a52ffeb.yaml diff --git a/doc/source/cli/data/glance.csv b/doc/source/cli/data/glance.csv index 12b6851d1a..26f720cdb3 100644 --- a/doc/source/cli/data/glance.csv +++ b/doc/source/cli/data/glance.csv @@ -8,7 +8,7 @@ image-import,,Initiate the image import taskflow. image-list,image list,List images you can access. image-reactivate,image set --activate,Reactivate specified image. image-show,image show,Describe a specific image. -image-stage,,Upload data for a specific image to staging. +image-stage,image stage,Upload data for a specific image to staging. image-tag-delete,image unset --tag ,Delete the tag associated with the given image. image-tag-update,image set --tag ,Update an image with the given tag. image-update,image set,Update an existing image. diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 53cfadede0..039f1d2dc8 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -1484,3 +1484,80 @@ def take_action(self, parsed_args): "Failed to unset %(propret)s of %(proptotal)s" " properties." ) % {'propret': propret, 'proptotal': proptotal} raise exceptions.CommandError(msg) + + +class StageImage(command.Command): + _description = _( + "Upload data for a specific image to staging.\n" + "This requires support for the interoperable image import process, " + "which was first introduced in Image API version 2.6 " + "(Glance 16.0.0 (Queens))" + ) + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + + parser.add_argument( + '--file', + metavar='', + dest='filename', + help=_( + 'Local file that contains disk image to be uploaded. ' + 'Alternatively, images can be passed via stdin.' + ), + ) + # NOTE(stephenfin): glanceclient had a --size argument but it didn't do + # anything so we have chosen not to port this + parser.add_argument( + '--progress', + action='store_true', + default=False, + help=_( + 'Show upload progress bar ' + '(ignored if passing data via stdin)' + ), + ) + parser.add_argument( + 'image', + metavar='', + help=_('Image to upload data for (name or ID)'), + ) + + return parser + + def take_action(self, parsed_args): + image_client = self.app.client_manager.image + + image = image_client.find_image( + parsed_args.image, + ignore_missing=False, + ) + # open the file first to ensure any failures are handled before the + # image is created. Get the file name (if it is file, and not stdin) + # for easier further handling. + if parsed_args.filename: + try: + fp = open(parsed_args.filename, 'rb') + except FileNotFoundError: + raise exceptions.CommandError( + '%r is not a valid file' % parsed_args.filename, + ) + else: + fp = get_data_from_stdin() + + kwargs = {} + + if parsed_args.progress and parsed_args.filename: + # NOTE(stephenfin): we only show a progress bar if the user + # requested it *and* we're reading from a file (not stdin) + filesize = os.path.getsize(parsed_args.filename) + if filesize is not None: + kwargs['data'] = progressbar.VerboseFileWrapper(fp, filesize) + else: + kwargs['data'] = fp + elif parsed_args.filename: + kwargs['filename'] = parsed_args.filename + elif fp: + kwargs['data'] = fp + + image_client.stage_image(image, **kwargs) diff --git a/openstackclient/tests/unit/image/v2/fakes.py b/openstackclient/tests/unit/image/v2/fakes.py index cf09df778a..8ce2a7d556 100644 --- a/openstackclient/tests/unit/image/v2/fakes.py +++ b/openstackclient/tests/unit/image/v2/fakes.py @@ -38,6 +38,7 @@ def __init__(self, **kwargs): self.download_image = mock.Mock() self.reactivate_image = mock.Mock() self.deactivate_image = mock.Mock() + self.stage_image = mock.Mock() self.members = mock.Mock() self.add_member = mock.Mock() diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index ac9ddae6e9..8dea7f05a3 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -22,7 +22,7 @@ from osc_lib.cli import format_columns from osc_lib import exceptions -from openstackclient.image.v2 import image +from openstackclient.image.v2 import image as _image from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes from openstackclient.tests.unit.image.v2 import fakes as image_fakes from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes @@ -73,10 +73,10 @@ def setUp(self): self.client.update_image.return_value = self.new_image (self.expected_columns, self.expected_data) = zip( - *sorted(image._format_image(self.new_image).items())) + *sorted(_image._format_image(self.new_image).items())) # Get the command object to test - self.cmd = image.CreateImage(self.app, None) + self.cmd = _image.CreateImage(self.app, None) @mock.patch("sys.stdin", side_effect=[None]) def test_image_reserve_no_options(self, raw_input): @@ -84,8 +84,8 @@ def test_image_reserve_no_options(self, raw_input): self.new_image.name ] verifylist = [ - ('container_format', image.DEFAULT_CONTAINER_FORMAT), - ('disk_format', image.DEFAULT_DISK_FORMAT), + ('container_format', _image.DEFAULT_CONTAINER_FORMAT), + ('disk_format', _image.DEFAULT_DISK_FORMAT), ('name', self.new_image.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -99,8 +99,8 @@ def test_image_reserve_no_options(self, raw_input): 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, + container_format=_image.DEFAULT_CONTAINER_FORMAT, + disk_format=_image.DEFAULT_DISK_FORMAT, ) self.assertEqual(self.expected_columns, columns) @@ -224,8 +224,8 @@ def test_image_create_file(self): 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, + container_format=_image.DEFAULT_CONTAINER_FORMAT, + disk_format=_image.DEFAULT_DISK_FORMAT, is_protected=self.new_image.is_protected, visibility=self.new_image.visibility, Alpha='1', @@ -245,7 +245,7 @@ def test_image_create_file(self): def test_image_create__progress_ignore_with_stdin( self, mock_get_data_from_stdin, ): - fake_stdin = io.StringIO('fake-image-data') + fake_stdin = io.BytesIO(b'some fake data') mock_get_data_from_stdin.return_value = fake_stdin arglist = [ @@ -263,8 +263,8 @@ def test_image_create__progress_ignore_with_stdin( 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, + container_format=_image.DEFAULT_CONTAINER_FORMAT, + disk_format=_image.DEFAULT_DISK_FORMAT, data=fake_stdin, validate_checksum=False, ) @@ -305,8 +305,8 @@ def test_image_create_import(self, raw_input): 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, + container_format=_image.DEFAULT_CONTAINER_FORMAT, + disk_format=_image.DEFAULT_DISK_FORMAT, use_import=True ) @@ -445,7 +445,7 @@ def setUp(self): self.project_mock.get.return_value = self.project self.domain_mock.get.return_value = self.domain # Get the command object to test - self.cmd = image.AddProjectToImage(self.app, None) + self.cmd = _image.AddProjectToImage(self.app, None) def test_add_project_to_image_no_option(self): arglist = [ @@ -504,7 +504,7 @@ def setUp(self): self.client.delete_image.return_value = None # Get the command object to test - self.cmd = image.DeleteImage(self.app, None) + self.cmd = _image.DeleteImage(self.app, None) def test_image_delete_no_options(self): images = self.setup_images_mock(count=1) @@ -595,7 +595,7 @@ def setUp(self): self.client.images.side_effect = [[self._image], []] # Get the command object to test - self.cmd = image.ListImage(self.app, None) + self.cmd = _image.ListImage(self.app, None) def test_image_list_no_options(self): arglist = [] @@ -993,7 +993,7 @@ def setUp(self): self.client.find_image.return_value = self._image self.client.members.return_value = [self.member] - self.cmd = image.ListImageProjects(self.app, None) + self.cmd = _image.ListImageProjects(self.app, None) def test_image_member_list(self): arglist = [ @@ -1028,7 +1028,7 @@ def setUp(self): self.domain_mock.get.return_value = self.domain self.client.remove_member.return_value = None # Get the command object to test - self.cmd = image.RemoveProjectImage(self.app, None) + self.cmd = _image.RemoveProjectImage(self.app, None) def test_remove_project_image_no_options(self): arglist = [ @@ -1095,7 +1095,7 @@ def setUp(self): ) # Get the command object to test - self.cmd = image.SetImage(self.app, None) + self.cmd = _image.SetImage(self.app, None) def test_image_set_no_options(self): arglist = [ @@ -1624,7 +1624,7 @@ def setUp(self): self.client.find_image = mock.Mock(return_value=self._data) # Get the command object to test - self.cmd = image.ShowImage(self.app, None) + self.cmd = _image.ShowImage(self.app, None) def test_image_show(self): arglist = [ @@ -1689,7 +1689,7 @@ def setUp(self): self.client.update_image.return_value = self.image # Get the command object to test - self.cmd = image.UnsetImage(self.app, None) + self.cmd = _image.UnsetImage(self.app, None) def test_image_unset_no_options(self): arglist = [ @@ -1769,6 +1769,60 @@ def test_image_unset_mixed_option(self): self.assertIsNone(result) +class TestImageStage(TestImage): + + image = image_fakes.create_one_image({}) + + def setUp(self): + super().setUp() + + self.client.find_image.return_value = self.image + + self.cmd = _image.StageImage(self.app, None) + + def test_stage_image__from_file(self): + imagefile = tempfile.NamedTemporaryFile(delete=False) + imagefile.write(b'\0') + imagefile.close() + + arglist = [ + '--file', imagefile.name, + self.image.name, + ] + verifylist = [ + ('filename', imagefile.name), + ('image', self.image.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.client.stage_image.assert_called_once_with( + self.image, + filename=imagefile.name, + ) + + @mock.patch('openstackclient.image.v2.image.get_data_from_stdin') + def test_stage_image__from_stdin(self, mock_get_data_from_stdin): + fake_stdin = io.BytesIO(b"some initial binary data: \x00\x01") + mock_get_data_from_stdin.return_value = fake_stdin + + arglist = [ + self.image.name, + ] + verifylist = [ + ('image', self.image.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.client.stage_image.assert_called_once_with( + self.image, + data=fake_stdin, + ) + + class TestImageSave(TestImage): image = image_fakes.create_one_image({}) @@ -1780,7 +1834,7 @@ def setUp(self): self.client.download_image.return_value = self.image # Get the command object to test - self.cmd = image.SaveImage(self.app, None) + self.cmd = _image.SaveImage(self.app, None) def test_save_data(self): @@ -1810,7 +1864,7 @@ def test_get_data_from_stdin(self): stdin.isatty.return_value = False stdin.buffer = fd - test_fd = image.get_data_from_stdin() + test_fd = _image.get_data_from_stdin() # Ensure data written to temp file is correct self.assertEqual(fd, test_fd) @@ -1822,6 +1876,6 @@ def test_get_data_from_stdin__interactive(self): # There is stdin, but interactive stdin.return_value = fd - test_fd = image.get_data_from_stdin() + test_fd = _image.get_data_from_stdin() self.assertIsNone(test_fd) diff --git a/releasenotes/notes/image-stage-ac19c47e6a52ffeb.yaml b/releasenotes/notes/image-stage-ac19c47e6a52ffeb.yaml new file mode 100644 index 0000000000..10bd0497ec --- /dev/null +++ b/releasenotes/notes/image-stage-ac19c47e6a52ffeb.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added a new command, ``image stage``, that will allow users to upload data + for an image to staging. diff --git a/setup.cfg b/setup.cfg index f8d0dffce5..7900bbe219 100644 --- a/setup.cfg +++ b/setup.cfg @@ -383,6 +383,7 @@ openstack.image.v2 = image_show = openstackclient.image.v2.image:ShowImage image_set = openstackclient.image.v2.image:SetImage image_unset = openstackclient.image.v2.image:UnsetImage + 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_list = openstackclient.image.v2.metadef_namespaces:ListMetadefNameSpaces From 4eea3408dc492e948671b625ffc4379212b5857c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 20 Oct 2022 13:49:17 +0100 Subject: [PATCH 056/595] image: Add 'image import' command Note that we require some additional functionality in SDK for this to work properly, but it's a start. Change-Id: I87f94db6cced67f36f71685e791416f9eed16bd0 Signed-off-by: Stephen Finucane --- doc/source/cli/data/glance.csv | 2 +- openstackclient/image/v2/image.py | 243 ++++++++++++++++++ openstackclient/tests/unit/image/v2/fakes.py | 37 ++- .../tests/unit/image/v2/test_image.py | 191 ++++++++++++++ .../notes/image-import-d5da3e5ce8733fb0.yaml | 6 + 5 files changed, 473 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/image-import-d5da3e5ce8733fb0.yaml diff --git a/doc/source/cli/data/glance.csv b/doc/source/cli/data/glance.csv index 26f720cdb3..d5c65f2dc3 100644 --- a/doc/source/cli/data/glance.csv +++ b/doc/source/cli/data/glance.csv @@ -4,7 +4,7 @@ image-create-via-import,,EXPERIMENTAL: Create a new image via image import. image-deactivate,image set --deactivate,Deactivate specified image. image-delete,image delete,Delete specified image. image-download,image save,Download a specific image. -image-import,,Initiate the image import taskflow. +image-import,image import,Initiate the image import taskflow. image-list,image list,List images you can access. image-reactivate,image set --activate,Reactivate specified image. image-show,image show,Describe a specific image. diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 039f1d2dc8..c2c0fe3943 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -22,6 +22,7 @@ import sys from cinderclient import api_versions +from openstack import exceptions as sdk_exceptions from openstack.image import image_signer from osc_lib.api import utils as api_utils from osc_lib.cli import format_columns @@ -1561,3 +1562,245 @@ def take_action(self, parsed_args): kwargs['data'] = fp image_client.stage_image(image, **kwargs) + + +class ImportImage(command.ShowOne): + _description = _( + "Initiate the image import process.\n" + "This requires support for the interoperable image import process, " + "which was first introduced in Image API version 2.6 " + "(Glance 16.0.0 (Queens))" + ) + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + + parser.add_argument( + 'image', + metavar='', + help=_('Image to initiate import process for (name or ID)'), + ) + # TODO(stephenfin): Uncomment help text when we have this command + # implemented + parser.add_argument( + '--method', + metavar='', + default='glance-direct', + dest='import_method', + choices=[ + 'glance-direct', + 'web-download', + 'glance-download', + 'copy-image', + ], + help=_( + "Import method used for image import process. " + "Not all deployments will support all methods. " + # "Valid values can be retrieved with the 'image import " + # "methods' command. " + "The 'glance-direct' method (default) requires images be " + "first staged using the 'image-stage' command." + ), + ) + parser.add_argument( + '--uri', + metavar='', + help=_( + "URI to download the external image " + "(only valid with the 'web-download' import method)" + ), + ) + parser.add_argument( + '--remote-image', + metavar='', + help=_( + "The image of remote glance (ID only) to be imported " + "(only valid with the 'glance-download' import method)" + ), + ) + parser.add_argument( + '--remote-region', + metavar='', + help=_( + "The remote Glance region to download the image from " + "(only valid with the 'glance-download' import method)" + ), + ) + parser.add_argument( + '--remote-service-interface', + metavar='', + help=_( + "The remote Glance service interface to use when importing " + "images " + "(only valid with the 'glance-download' import method)" + ), + ) + stores_group = parser.add_mutually_exclusive_group() + stores_group.add_argument( + '--store', + metavar='', + dest='stores', + nargs='*', + help=_( + "Backend store to upload image to " + "(specify multiple times to upload to multiple stores) " + "(either '--store' or '--all-stores' required with the " + "'copy-image' import method)" + ), + ) + stores_group.add_argument( + '--all-stores', + help=_( + "Make image available to all stores " + "(either '--store' or '--all-stores' required with the " + "'copy-image' import method)" + ), + ) + parser.add_argument( + '--allow-failure', + action='store_true', + dest='allow_failure', + default=True, + help=_( + 'When uploading to multiple stores, indicate that the import ' + 'should be continue should any of the uploads fail. ' + 'Only usable with --stores or --all-stores' + ), + ) + parser.add_argument( + '--disallow-failure', + action='store_true', + dest='allow_failure', + default=True, + help=_( + 'When uploading to multiple stores, indicate that the import ' + 'should be reverted should any of the uploads fail. ' + 'Only usable with --stores or --all-stores' + ), + ) + parser.add_argument( + '--wait', + action='store_true', + help=_('Wait for operation to complete'), + ) + return parser + + def take_action(self, parsed_args): + image_client = self.app.client_manager.image + + try: + import_info = image_client.get_import_info() + except sdk_exceptions.ResourceNotFound: + msg = _( + 'The Image Import feature is not supported by this deployment' + ) + raise exceptions.CommandError(msg) + + import_methods = import_info.import_methods['value'] + + if parsed_args.import_method not in import_methods: + msg = _( + "The '%s' import method is not supported by this deployment. " + "Supported: %s" + ) + raise exceptions.CommandError( + msg % (parsed_args.import_method, ', '.join(import_methods)), + ) + + if parsed_args.import_method == 'web-download': + if not parsed_args.uri: + msg = _( + "The '--uri' option is required when using " + "'--method=web-download'" + ) + raise exceptions.CommandError(msg) + else: + if parsed_args.uri: + msg = _( + "The '--uri' option is only supported when using " + "'--method=web-download'" + ) + raise exceptions.CommandError(msg) + + if parsed_args.import_method == 'glance-download': + if not (parsed_args.remote_region and parsed_args.remote_image): + msg = _( + "The '--remote-region' and '--remote-image' options are " + "required when using '--method=web-download'" + ) + raise exceptions.CommandError(msg) + else: + if parsed_args.remote_region: + msg = _( + "The '--remote-region' option is only supported when " + "using '--method=glance-download'" + ) + raise exceptions.CommandError(msg) + + if parsed_args.remote_image: + msg = _( + "The '--remote-image' option is only supported when using " + "'--method=glance-download'" + ) + raise exceptions.CommandError(msg) + + if parsed_args.remote_service_interface: + msg = _( + "The '--remote-service-interface' option is only " + "supported when using '--method=glance-download'" + ) + raise exceptions.CommandError(msg) + + if parsed_args.import_method == 'copy-image': + if not (parsed_args.stores or parsed_args.all_stores): + msg = _( + "The '--stores' or '--all-stores' options are required " + "when using '--method=copy-image'" + ) + raise exceptions.CommandError(msg) + + image = image_client.find_image(parsed_args.image) + + if not image.container_format and not image.disk_format: + msg = _( + "The 'container_format' and 'disk_format' properties " + "must be set on an image before it can be imported" + ) + raise exceptions.CommandError(msg) + + if parsed_args.import_method == 'glance-direct': + if image.status != 'uploading': + msg = _( + "The 'glance-direct' import method can only be used with " + "an image in status 'uploading'" + ) + raise exceptions.CommandError(msg) + elif parsed_args.import_method == 'web-download': + if image.status != 'queued': + msg = _( + "The 'web-download' import method can only be used with " + "an image in status 'queued'" + ) + raise exceptions.CommandError(msg) + elif parsed_args.import_method == 'copy-image': + if image.status != 'active': + msg = _( + "The 'copy-image' import method can only be used with " + "an image in status 'active'" + ) + raise exceptions.CommandError(msg) + + image_client.import_image( + image, + method=parsed_args.import_method, + # uri=parsed_args.uri, + # remote_region=parsed_args.remote_region, + # remote_image=parsed_args.remote_image, + # remote_service_interface=parsed_args.remote_service_interface, + stores=parsed_args.stores, + all_stores=parsed_args.all_stores, + all_stores_must_succeed=not parsed_args.allow_failure, + ) + + info = _format_image(image) + return zip(*sorted(info.items())) diff --git a/openstackclient/tests/unit/image/v2/fakes.py b/openstackclient/tests/unit/image/v2/fakes.py index 8ce2a7d556..ded9ff313a 100644 --- a/openstackclient/tests/unit/image/v2/fakes.py +++ b/openstackclient/tests/unit/image/v2/fakes.py @@ -19,6 +19,7 @@ from openstack.image.v2 import image from openstack.image.v2 import member from openstack.image.v2 import metadef_namespace +from openstack.image.v2 import service_info as _service_info from openstack.image.v2 import task from openstackclient.tests.unit import fakes @@ -39,6 +40,7 @@ def __init__(self, **kwargs): self.reactivate_image = mock.Mock() self.deactivate_image = mock.Mock() self.stage_image = mock.Mock() + self.import_image = mock.Mock() self.members = mock.Mock() self.add_member = mock.Mock() @@ -49,17 +51,15 @@ def __init__(self, **kwargs): self.metadef_namespaces = mock.Mock() self.tasks = mock.Mock() + self.tasks.resource_class = fakes.FakeResource(None, {}) self.get_task = mock.Mock() + self.get_import_info = mock.Mock() + self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] self.version = 2.0 - self.tasks = mock.Mock() - self.tasks.resource_class = fakes.FakeResource(None, {}) - - self.metadef_namespaces = mock.Mock() - class TestImagev2(utils.TestCommand): @@ -143,6 +143,33 @@ def create_one_image_member(attrs=None): return member.Member(**image_member_info) +def create_one_import_info(attrs=None): + """Create a fake import info. + + :param attrs: A dictionary with all attributes of import info + :type attrs: dict + :return: A fake Import object. + :rtype: `openstack.image.v2.service_info.Import` + """ + attrs = attrs or {} + + import_info = { + 'import-methods': { + 'description': 'Import methods available.', + 'type': 'array', + 'value': [ + 'glance-direct', + 'web-download', + 'glance-download', + 'copy-image', + ] + } + } + import_info.update(attrs) + + return _service_info.Import(**import_info) + + def create_one_task(attrs=None): """Create a fake task. diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index 8dea7f05a3..010c4a9d35 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -1823,6 +1823,197 @@ def test_stage_image__from_stdin(self, mock_get_data_from_stdin): ) +class TestImageImport(TestImage): + + image = image_fakes.create_one_image( + { + 'container_format': 'bare', + 'disk_format': 'qcow2', + } + ) + import_info = image_fakes.create_one_import_info() + + def setUp(self): + super().setUp() + + self.client.find_image.return_value = self.image + self.client.get_import_info.return_value = self.import_info + + self.cmd = _image.ImportImage(self.app, None) + + def test_import_image__glance_direct(self): + self.image.status = 'uploading' + arglist = [ + self.image.name, + ] + verifylist = [ + ('image', self.image.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.client.import_image.assert_called_once_with( + self.image, + method='glance-direct', + stores=None, + all_stores=None, + all_stores_must_succeed=False, + ) + + def test_import_image__web_download(self): + self.image.status = 'queued' + arglist = [ + self.image.name, + '--method', 'web-download', + '--uri', 'https://example.com/', + ] + verifylist = [ + ('image', self.image.name), + ('import_method', 'web-download'), + ('uri', 'https://example.com/'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.client.import_image.assert_called_once_with( + self.image, + method='web-download', + # uri='https://example.com/', + stores=None, + all_stores=None, + all_stores_must_succeed=False, + ) + + # NOTE(stephenfin): We don't do this for all combinations since that would + # be tedious af. You get the idea... + def test_import_image__web_download_missing_options(self): + arglist = [ + self.image.name, + '--method', 'web-download', + ] + verifylist = [ + ('image', self.image.name), + ('import_method', 'web-download'), + ('uri', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + self.assertIn("The '--uri' option is required ", str(exc)) + + self.client.import_image.assert_not_called() + + # NOTE(stephenfin): Ditto + def test_import_image__web_download_invalid_options(self): + arglist = [ + self.image.name, + '--method', 'glance-direct', # != web-download + '--uri', 'https://example.com/', + ] + verifylist = [ + ('image', self.image.name), + ('import_method', 'glance-direct'), + ('uri', 'https://example.com/'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + self.assertIn("The '--uri' option is only supported ", str(exc)) + + self.client.import_image.assert_not_called() + + def test_import_image__web_download_invalid_image_state(self): + self.image.status = 'uploading' # != 'queued' + arglist = [ + self.image.name, + '--method', 'web-download', + '--uri', 'https://example.com/', + ] + verifylist = [ + ('image', self.image.name), + ('import_method', 'web-download'), + ('uri', 'https://example.com/'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + self.assertIn( + "The 'web-download' import method can only be used with " + "an image in status 'queued'", + str(exc), + ) + + self.client.import_image.assert_not_called() + + def test_import_image__copy_image(self): + self.image.status = 'active' + arglist = [ + self.image.name, + '--method', 'copy-image', + '--store', 'fast', + ] + verifylist = [ + ('image', self.image.name), + ('import_method', 'copy-image'), + ('stores', ['fast']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.client.import_image.assert_called_once_with( + self.image, + method='copy-image', + stores=['fast'], + all_stores=None, + all_stores_must_succeed=False, + ) + + def test_import_image__glance_download(self): + arglist = [ + self.image.name, + '--method', 'glance-download', + '--remote-region', 'eu/dublin', + '--remote-image', 'remote-image-id', + '--remote-service-interface', 'private', + ] + verifylist = [ + ('image', self.image.name), + ('import_method', 'glance-download'), + ('remote_region', 'eu/dublin'), + ('remote_image', 'remote-image-id'), + ('remote_service_interface', 'private'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.client.import_image.assert_called_once_with( + self.image, + method='glance-download', + # remote_region='eu/dublin', + # remote_image='remote-image-id', + # remote_service_interface='private', + stores=None, + all_stores=None, + all_stores_must_succeed=False, + ) + + class TestImageSave(TestImage): image = image_fakes.create_one_image({}) diff --git a/releasenotes/notes/image-import-d5da3e5ce8733fb0.yaml b/releasenotes/notes/image-import-d5da3e5ce8733fb0.yaml new file mode 100644 index 0000000000..0c394c82d1 --- /dev/null +++ b/releasenotes/notes/image-import-d5da3e5ce8733fb0.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add ``image import`` command, allowing users to take advantage of the + interoperable image import functionality first introduced in Glance 16.0.0 + (Queens). From 006e35509d3cea66dc13fe2238dac92f47fe3c5c Mon Sep 17 00:00:00 2001 From: Violet Kurtz Date: Mon, 15 Aug 2022 23:52:55 +0000 Subject: [PATCH 057/595] Moved hypervisor to the SDK Change-Id: Ie955fb4d27c30e044626732a1f3e0f141cb85aa5 --- openstackclient/compute/v2/hypervisor.py | 137 ++++++++--- .../tests/unit/compute/v2/fakes.py | 134 +++++----- .../tests/unit/compute/v2/test_hypervisor.py | 230 +++++++++++------- ...ch-hypervisor-to-sdk-2e90b26a14ffcef3.yaml | 3 + 4 files changed, 303 insertions(+), 201 deletions(-) create mode 100644 releasenotes/notes/switch-hypervisor-to-sdk-2e90b26a14ffcef3.yaml diff --git a/openstackclient/compute/v2/hypervisor.py b/openstackclient/compute/v2/hypervisor.py index 5f7497b5b1..d4b4003bf9 100644 --- a/openstackclient/compute/v2/hypervisor.py +++ b/openstackclient/compute/v2/hypervisor.py @@ -18,8 +18,8 @@ import json import re -from novaclient import api_versions from novaclient import exceptions as nova_exceptions +from openstack import utils as sdk_utils from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions @@ -28,11 +28,44 @@ from openstackclient.i18n import _ +def _get_hypervisor_columns(item, client): + column_map = {'name': 'hypervisor_hostname'} + hidden_columns = ['location', 'servers'] + + if sdk_utils.supports_microversion(client, '2.88'): + hidden_columns.extend([ + 'current_workload', + 'disk_available', + 'local_disk_free', + 'local_disk_size', + 'local_disk_used', + 'memory_free', + 'memory_size', + 'memory_used', + 'running_vms', + 'vcpus_used', + 'vcpus', + ]) + else: + column_map.update({ + '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_used': 'memory_mb_used', + 'memory_size': 'memory_mb', + }) + + return utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden_columns) + + class ListHypervisor(command.Lister): _description = _("List hypervisors") def get_parser(self, prog_name): - parser = super(ListHypervisor, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( '--matching', metavar='', @@ -67,7 +100,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - compute_client = self.app.client_manager.compute + compute_client = self.app.client_manager.sdk_connection.compute list_opts = {} @@ -78,7 +111,7 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) if parsed_args.marker: - if compute_client.api_version < api_versions.APIVersion('2.33'): + if not sdk_utils.supports_microversion(compute_client, '2.33'): msg = _( '--os-compute-api-version 2.33 or greater is required to ' 'support the --marker option' @@ -87,7 +120,7 @@ def take_action(self, parsed_args): list_opts['marker'] = parsed_args.marker if parsed_args.limit: - if compute_client.api_version < api_versions.APIVersion('2.33'): + if not sdk_utils.supports_microversion(compute_client, '2.33'): msg = _( '--os-compute-api-version 2.33 or greater is required to ' 'support the --limit option' @@ -95,23 +128,43 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) list_opts['limit'] = parsed_args.limit - columns = ( + column_headers = ( "ID", "Hypervisor Hostname", "Hypervisor Type", "Host IP", "State" ) + columns = ( + 'id', + 'name', + 'hypervisor_type', + 'host_ip', + 'state' + ) if parsed_args.long: - columns += ("vCPUs Used", "vCPUs", "Memory MB Used", "Memory MB") + if not sdk_utils.supports_microversion(compute_client, '2.88'): + column_headers += ( + 'vCPUs Used', + 'vCPUs', + 'Memory MB Used', + 'Memory MB' + ) + columns += ( + 'vcpus_used', + 'vcpus', + 'memory_used', + 'memory_size' + ) if parsed_args.matching: - data = compute_client.hypervisors.search(parsed_args.matching) + data = compute_client.find_hypervisor( + parsed_args.matching, ignore_missing=False) else: - data = compute_client.hypervisors.list(**list_opts) + data = compute_client.hypervisors(**list_opts, details=True) return ( - columns, + column_headers, (utils.get_item_properties(s, columns) for s in data), ) @@ -120,7 +173,7 @@ class ShowHypervisor(command.ShowOne): _description = _("Display hypervisor details") def get_parser(self, prog_name): - parser = super(ShowHypervisor, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( "hypervisor", metavar="", @@ -129,20 +182,25 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - compute_client = self.app.client_manager.compute - hypervisor = utils.find_resource(compute_client.hypervisors, - parsed_args.hypervisor)._info.copy() + compute_client = self.app.client_manager.sdk_connection.compute + hypervisor = compute_client.find_hypervisor( + parsed_args.hypervisor, ignore_missing=False).copy() + + # Some of the properties in the hypervisor object need to be processed + # before they get reported to the user. We spend this section + # extracting the relevant details to be reported by modifying our + # copy of the hypervisor object. + aggregates = compute_client.aggregates() + hypervisor['aggregates'] = list() + service_details = hypervisor['service_details'] - aggregates = compute_client.aggregates.list() - hypervisor["aggregates"] = list() if aggregates: # Hypervisors in nova cells are prefixed by "@" - if "@" in hypervisor['service']['host']: - cell, service_host = hypervisor['service']['host'].split( - '@', 1) + if "@" in service_details['host']: + cell, service_host = service_details['host'].split('@', 1) else: cell = None - service_host = hypervisor['service']['host'] + service_host = service_details['host'] if cell: # The host aggregates are also prefixed by "@" @@ -154,42 +212,45 @@ def take_action(self, parsed_args): member_of = [aggregate.name for aggregate in aggregates if service_host in aggregate.hosts] - hypervisor["aggregates"] = member_of + hypervisor['aggregates'] = member_of try: - uptime = compute_client.hypervisors.uptime(hypervisor['id'])._info + if sdk_utils.supports_microversion(compute_client, '2.88'): + uptime = hypervisor['uptime'] or '' + del hypervisor['uptime'] + else: + del hypervisor['uptime'] + uptime = compute_client.get_hypervisor_uptime( + hypervisor['id'])['uptime'] # Extract data from uptime value # format: 0 up 0, 0 users, load average: 0, 0, 0 # example: 17:37:14 up 2:33, 3 users, # load average: 0.33, 0.36, 0.34 m = re.match( r"\s*(.+)\sup\s+(.+),\s+(.+)\susers?,\s+load average:\s(.+)", - uptime['uptime']) + uptime) if m: - hypervisor["host_time"] = m.group(1) - hypervisor["uptime"] = m.group(2) - hypervisor["users"] = m.group(3) - hypervisor["load_average"] = m.group(4) + hypervisor['host_time'] = m.group(1) + hypervisor['uptime'] = m.group(2) + hypervisor['users'] = m.group(3) + hypervisor['load_average'] = m.group(4) except nova_exceptions.HTTPNotImplemented: pass - hypervisor["service_id"] = hypervisor["service"]["id"] - hypervisor["service_host"] = hypervisor["service"]["host"] - del hypervisor["service"] + hypervisor['service_id'] = service_details['id'] + hypervisor['service_host'] = service_details['host'] + del hypervisor['service_details'] - if compute_client.api_version < api_versions.APIVersion('2.28'): + if not sdk_utils.supports_microversion(compute_client, '2.28'): # microversion 2.28 transformed this to a JSON blob rather than a # string; on earlier fields, do this manually - if hypervisor['cpu_info']: - hypervisor['cpu_info'] = json.loads(hypervisor['cpu_info']) - else: - hypervisor['cpu_info'] = {} - - columns = tuple(sorted(hypervisor)) + hypervisor['cpu_info'] = json.loads(hypervisor['cpu_info'] or '{}') + display_columns, columns = _get_hypervisor_columns( + hypervisor, compute_client) data = utils.get_dict_properties( hypervisor, columns, formatters={ 'cpu_info': format_columns.DictColumn, }) - return (columns, data) + return display_columns, data diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index d77797abd6..7d8c29ad7c 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -20,6 +20,7 @@ from novaclient import api_versions from openstack.compute.v2 import flavor as _flavor +from openstack.compute.v2 import hypervisor as _hypervisor from openstack.compute.v2 import server from openstack.compute.v2 import server_group as _server_group from openstack.compute.v2 import server_interface as _server_interface @@ -340,75 +341,6 @@ def create_one_extension(attrs=None): return extension -class FakeHypervisor(object): - """Fake one or more hypervisor.""" - - @staticmethod - def create_one_hypervisor(attrs=None): - """Create a fake hypervisor. - - :param dict attrs: - A dictionary with all attributes - :return: - A FakeResource object, with id, hypervisor_hostname, and so on - """ - attrs = attrs or {} - - # Set default attributes. - hypervisor_info = { - 'id': 'hypervisor-id-' + uuid.uuid4().hex, - 'hypervisor_hostname': 'hypervisor-hostname-' + uuid.uuid4().hex, - 'status': 'enabled', - 'host_ip': '192.168.0.10', - 'cpu_info': { - 'aaa': 'aaa', - }, - 'free_disk_gb': 50, - 'hypervisor_version': 2004001, - 'disk_available_least': 50, - 'local_gb': 50, - 'free_ram_mb': 1024, - 'service': { - 'host': 'aaa', - 'disabled_reason': None, - 'id': 1, - }, - 'vcpus_used': 0, - 'hypervisor_type': 'QEMU', - 'local_gb_used': 0, - 'vcpus': 4, - 'memory_mb_used': 512, - 'memory_mb': 1024, - 'current_workload': 0, - 'state': 'up', - 'running_vms': 0, - } - - # Overwrite default attributes. - hypervisor_info.update(attrs) - - hypervisor = fakes.FakeResource(info=copy.deepcopy(hypervisor_info), - loaded=True) - return hypervisor - - @staticmethod - def create_hypervisors(attrs=None, count=2): - """Create multiple fake hypervisors. - - :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(FakeHypervisor.create_one_hypervisor(attrs)) - - return hypervisors - - class FakeHypervisorStats(object): """Fake one or more hypervisor stats.""" @@ -1795,6 +1727,70 @@ def create_sdk_volume_attachments(attrs=None, methods=None, count=2): return volume_attachments +def create_one_hypervisor(attrs=None): + """Create a fake hypervisor. + + :param dict attrs: + A dictionary with all attributes + :return: + A FakeResource object, with id, hypervisor_hostname, and so on + """ + attrs = attrs or {} + + # Set default attributes. + hypervisor_info = { + 'id': 'hypervisor-id-' + uuid.uuid4().hex, + 'hypervisor_hostname': 'hypervisor-hostname-' + uuid.uuid4().hex, + 'status': 'enabled', + 'host_ip': '192.168.0.10', + 'cpu_info': { + 'aaa': 'aaa', + }, + 'free_disk_gb': 50, + 'hypervisor_version': 2004001, + 'disk_available_least': 50, + 'local_gb': 50, + 'free_ram_mb': 1024, + 'service': { + 'host': 'aaa', + 'disabled_reason': None, + 'id': 1, + }, + 'vcpus_used': 0, + 'hypervisor_type': 'QEMU', + 'local_gb_used': 0, + 'vcpus': 4, + 'memory_mb_used': 512, + 'memory_mb': 1024, + 'current_workload': 0, + 'state': 'up', + 'running_vms': 0, + } + + # Overwrite default attributes. + hypervisor_info.update(attrs) + + hypervisor = _hypervisor.Hypervisor(**hypervisor_info, loaded=True) + return hypervisor + + +def create_hypervisors(attrs=None, count=2): + """Create multiple fake hypervisors. + + :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(create_one_hypervisor(attrs)) + + return hypervisors + + def create_one_server_group(attrs=None): """Create a fake server group diff --git a/openstackclient/tests/unit/compute/v2/test_hypervisor.py b/openstackclient/tests/unit/compute/v2/test_hypervisor.py index 7dbd6e1983..e5804665c1 100644 --- a/openstackclient/tests/unit/compute/v2/test_hypervisor.py +++ b/openstackclient/tests/unit/compute/v2/test_hypervisor.py @@ -13,41 +13,37 @@ # under the License. # -import copy import json +from unittest import mock -from novaclient import api_versions from novaclient import exceptions as nova_exceptions +from openstack import utils as sdk_utils from osc_lib.cli import format_columns from osc_lib import exceptions from openstackclient.compute.v2 import hypervisor from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes -from openstackclient.tests.unit import fakes class TestHypervisor(compute_fakes.TestComputev2): def setUp(self): - super(TestHypervisor, self).setUp() + super().setUp() - # Get a shortcut to the compute client hypervisors mock - self.hypervisors_mock = self.app.client_manager.compute.hypervisors - self.hypervisors_mock.reset_mock() - - # Get a shortcut to the compute client aggregates mock - self.aggregates_mock = self.app.client_manager.compute.aggregates - self.aggregates_mock.reset_mock() + # Create and get a shortcut to the compute client mock + self.app.client_manager.sdk_connection = mock.Mock() + self.sdk_client = self.app.client_manager.sdk_connection.compute + self.sdk_client.reset_mock() class TestHypervisorList(TestHypervisor): def setUp(self): - super(TestHypervisorList, self).setUp() + super().setUp() # Fake hypervisors to be listed up - self.hypervisors = compute_fakes.FakeHypervisor.create_hypervisors() - self.hypervisors_mock.list.return_value = self.hypervisors + self.hypervisors = compute_fakes.create_hypervisors() + self.sdk_client.hypervisors.return_value = self.hypervisors self.columns = ( "ID", @@ -70,14 +66,14 @@ def setUp(self): self.data = ( ( self.hypervisors[0].id, - self.hypervisors[0].hypervisor_hostname, + self.hypervisors[0].name, self.hypervisors[0].hypervisor_type, self.hypervisors[0].host_ip, self.hypervisors[0].state ), ( self.hypervisors[1].id, - self.hypervisors[1].hypervisor_hostname, + self.hypervisors[1].name, self.hypervisors[1].hypervisor_type, self.hypervisors[1].host_ip, self.hypervisors[1].state @@ -87,25 +83,25 @@ def setUp(self): self.data_long = ( ( self.hypervisors[0].id, - self.hypervisors[0].hypervisor_hostname, + self.hypervisors[0].name, self.hypervisors[0].hypervisor_type, self.hypervisors[0].host_ip, self.hypervisors[0].state, self.hypervisors[0].vcpus_used, self.hypervisors[0].vcpus, - self.hypervisors[0].memory_mb_used, - self.hypervisors[0].memory_mb + self.hypervisors[0].memory_used, + self.hypervisors[0].memory_size ), ( self.hypervisors[1].id, - self.hypervisors[1].hypervisor_hostname, + self.hypervisors[1].name, self.hypervisors[1].hypervisor_type, self.hypervisors[1].host_ip, self.hypervisors[1].state, self.hypervisors[1].vcpus_used, self.hypervisors[1].vcpus, - self.hypervisors[1].memory_mb_used, - self.hypervisors[1].memory_mb + self.hypervisors[1].memory_used, + self.hypervisors[1].memory_size ), ) # Get the command object to test @@ -121,25 +117,25 @@ def test_hypervisor_list_no_option(self): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.hypervisors_mock.list.assert_called_with() + self.sdk_client.hypervisors.assert_called_with(details=True) self.assertEqual(self.columns, columns) self.assertEqual(self.data, tuple(data)) def test_hypervisor_list_matching_option_found(self): arglist = [ - '--matching', self.hypervisors[0].hypervisor_hostname, + '--matching', self.hypervisors[0].name, ] verifylist = [ - ('matching', self.hypervisors[0].hypervisor_hostname), + ('matching', self.hypervisors[0].name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # Fake the return value of search() - self.hypervisors_mock.search.return_value = [self.hypervisors[0]] + self.sdk_client.find_hypervisor.return_value = [self.hypervisors[0]] self.data = ( ( self.hypervisors[0].id, - self.hypervisors[0].hypervisor_hostname, + self.hypervisors[0].name, self.hypervisors[1].hypervisor_type, self.hypervisors[1].host_ip, self.hypervisors[1].state, @@ -151,8 +147,9 @@ def test_hypervisor_list_matching_option_found(self): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.hypervisors_mock.search.assert_called_with( - self.hypervisors[0].hypervisor_hostname + self.sdk_client.find_hypervisor.assert_called_with( + self.hypervisors[0].name, + ignore_missing=False ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, tuple(data)) @@ -167,25 +164,25 @@ def test_hypervisor_list_matching_option_not_found(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) # Fake exception raised from search() - self.hypervisors_mock.search.side_effect = exceptions.NotFound(None) + self.sdk_client.find_hypervisor.side_effect = \ + exceptions.NotFound(None) self.assertRaises(exceptions.NotFound, self.cmd.take_action, parsed_args) - def test_hypervisor_list_with_matching_and_pagination_options(self): - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.32') - + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=False) + def test_hypervisor_list_with_matching_and_pagination_options( + self, sm_mock): arglist = [ - '--matching', self.hypervisors[0].hypervisor_hostname, + '--matching', self.hypervisors[0].name, '--limit', '1', - '--marker', self.hypervisors[0].hypervisor_hostname, + '--marker', self.hypervisors[0].name, ] verifylist = [ - ('matching', self.hypervisors[0].hypervisor_hostname), + ('matching', self.hypervisors[0].name), ('limit', 1), - ('marker', self.hypervisors[0].hypervisor_hostname), + ('marker', self.hypervisors[0].name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -197,7 +194,8 @@ def test_hypervisor_list_with_matching_and_pagination_options(self): self.assertIn( '--matching is not compatible with --marker or --limit', str(ex)) - def test_hypervisor_list_long_option(self): + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=False) + def test_hypervisor_list_long_option(self, sm_mock): arglist = [ '--long', ] @@ -211,14 +209,12 @@ def test_hypervisor_list_long_option(self): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.hypervisors_mock.list.assert_called_with() + self.sdk_client.hypervisors.assert_called_with(details=True) self.assertEqual(self.columns_long, columns) self.assertEqual(self.data_long, tuple(data)) - def test_hypervisor_list_with_limit(self): - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.33') - + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=True) + def test_hypervisor_list_with_limit(self, sm_mock): arglist = [ '--limit', '1', ] @@ -229,12 +225,10 @@ def test_hypervisor_list_with_limit(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) - self.hypervisors_mock.list.assert_called_with(limit=1) - - def test_hypervisor_list_with_limit_pre_v233(self): - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.32') + self.sdk_client.hypervisors.assert_called_with(limit=1, details=True) + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=False) + def test_hypervisor_list_with_limit_pre_v233(self, sm_mock): arglist = [ '--limit', '1', ] @@ -251,10 +245,8 @@ def test_hypervisor_list_with_limit_pre_v233(self): self.assertIn( '--os-compute-api-version 2.33 or greater is required', str(ex)) - def test_hypervisor_list_with_marker(self): - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.33') - + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=True) + def test_hypervisor_list_with_marker(self, sm_mock): arglist = [ '--marker', 'test_hyp', ] @@ -265,12 +257,11 @@ def test_hypervisor_list_with_marker(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) - self.hypervisors_mock.list.assert_called_with(marker='test_hyp') - - def test_hypervisor_list_with_marker_pre_v233(self): - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.32') + self.sdk_client.hypervisors.assert_called_with( + marker='test_hyp', details=True) + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=False) + def test_hypervisor_list_with_marker_pre_v233(self, sm_mock): arglist = [ '--marker', 'test_hyp', ] @@ -291,29 +282,66 @@ def test_hypervisor_list_with_marker_pre_v233(self): class TestHypervisorShow(TestHypervisor): def setUp(self): - super(TestHypervisorShow, self).setUp() + super().setUp() + + uptime_string = (' 01:28:24 up 3 days, 11:15, 1 user, ' + ' load average: 0.94, 0.62, 0.50\n') # Fake hypervisors to be listed up - self.hypervisor = compute_fakes.FakeHypervisor.create_one_hypervisor() + self.hypervisor = compute_fakes.create_one_hypervisor(attrs={ + 'uptime': uptime_string, + }) - # Return value of utils.find_resource() - self.hypervisors_mock.get.return_value = self.hypervisor + # Return value of compute_client.find_hypervisor + self.sdk_client.find_hypervisor.return_value = self.hypervisor - # Return value of compute_client.aggregates.list() - self.aggregates_mock.list.return_value = [] + # Return value of compute_client.aggregates() + self.sdk_client.aggregates.return_value = [] - # Return value of compute_client.hypervisors.uptime() + # Return value of compute_client.get_hypervisor_uptime() uptime_info = { 'status': self.hypervisor.status, 'state': self.hypervisor.state, 'id': self.hypervisor.id, - 'hypervisor_hostname': self.hypervisor.hypervisor_hostname, - 'uptime': ' 01:28:24 up 3 days, 11:15, 1 user, ' - ' load average: 0.94, 0.62, 0.50\n', + 'hypervisor_hostname': self.hypervisor.name, + 'uptime': uptime_string, } - self.hypervisors_mock.uptime.return_value = fakes.FakeResource( - info=copy.deepcopy(uptime_info), - loaded=True + self.sdk_client.get_hypervisor_uptime.return_value = uptime_info + + self.columns_v288 = ( + 'aggregates', + 'cpu_info', + 'host_ip', + 'host_time', + 'hypervisor_hostname', + 'hypervisor_type', + 'hypervisor_version', + 'id', + 'load_average', + 'service_host', + 'service_id', + 'state', + 'status', + 'uptime', + 'users', + ) + + self.data_v288 = ( + [], + format_columns.DictColumn({'aaa': 'aaa'}), + '192.168.0.10', + '01:28:24', + self.hypervisor.name, + 'QEMU', + 2004001, + self.hypervisor.id, + '0.94, 0.62, 0.50', + 'aaa', + 1, + 'up', + 'enabled', + '3 days, 11:15', + '1', ) self.columns = ( @@ -353,7 +381,7 @@ def setUp(self): 1024, '192.168.0.10', '01:28:24', - self.hypervisor.hypervisor_hostname, + self.hypervisor.name, 'QEMU', 2004001, self.hypervisor.id, @@ -376,15 +404,32 @@ def setUp(self): # Get the command object to test self.cmd = hypervisor.ShowHypervisor(self.app, None) - def test_hypervisor_show(self): - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.28') + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=True) + def test_hypervisor_show(self, sm_mock): + arglist = [ + self.hypervisor.name, + ] + verifylist = [ + ('hypervisor', self.hypervisor.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(self.columns_v288, columns) + self.assertCountEqual(self.data_v288, data) + + @mock.patch.object(sdk_utils, 'supports_microversion', + side_effect=[False, True, False]) + def test_hypervisor_show_pre_v288(self, sm_mock): arglist = [ - self.hypervisor.hypervisor_hostname, + self.hypervisor.name, ] verifylist = [ - ('hypervisor', self.hypervisor.hypervisor_hostname), + ('hypervisor', self.hypervisor.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -396,21 +441,19 @@ def test_hypervisor_show(self): self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, data) - def test_hypervisor_show_pre_v228(self): - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.27') - + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=False) + def test_hypervisor_show_pre_v228(self, sm_mock): # before microversion 2.28, nova returned a stringified version of this # field - self.hypervisor._info['cpu_info'] = json.dumps( - self.hypervisor._info['cpu_info']) - self.hypervisors_mock.get.return_value = self.hypervisor + self.hypervisor.cpu_info = json.dumps( + self.hypervisor.cpu_info) + self.sdk_client.find_hypervisor.return_value = self.hypervisor arglist = [ - self.hypervisor.hypervisor_hostname, + self.hypervisor.name, ] verifylist = [ - ('hypervisor', self.hypervisor.hypervisor_hostname), + ('hypervisor', self.hypervisor.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -422,19 +465,18 @@ def test_hypervisor_show_pre_v228(self): self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, data) - def test_hypervisor_show_uptime_not_implemented(self): - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.28') - + @mock.patch.object(sdk_utils, 'supports_microversion', + side_effect=[False, True, False]) + def test_hypervisor_show_uptime_not_implemented(self, sm_mock): arglist = [ - self.hypervisor.hypervisor_hostname, + self.hypervisor.name, ] verifylist = [ - ('hypervisor', self.hypervisor.hypervisor_hostname), + ('hypervisor', self.hypervisor.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.hypervisors_mock.uptime.side_effect = ( + self.sdk_client.get_hypervisor_uptime.side_effect = ( nova_exceptions.HTTPNotImplemented(501)) # In base command class ShowOne in cliff, abstract method take_action() @@ -474,7 +516,7 @@ def test_hypervisor_show_uptime_not_implemented(self): 50, 1024, '192.168.0.10', - self.hypervisor.hypervisor_hostname, + self.hypervisor.name, 'QEMU', 2004001, self.hypervisor.id, diff --git a/releasenotes/notes/switch-hypervisor-to-sdk-2e90b26a14ffcef3.yaml b/releasenotes/notes/switch-hypervisor-to-sdk-2e90b26a14ffcef3.yaml new file mode 100644 index 0000000000..c5167929e7 --- /dev/null +++ b/releasenotes/notes/switch-hypervisor-to-sdk-2e90b26a14ffcef3.yaml @@ -0,0 +1,3 @@ +--- +features: + - Switch hypervisor to the OpenStackSDK From 348eb796321c8475af73b727a310c3a09f519ffa Mon Sep 17 00:00:00 2001 From: Jadon Naas Date: Thu, 10 Nov 2022 11:51:18 -0500 Subject: [PATCH 058/595] Docstring fix for CreateVolumeAttachment class The command "volume attachment create" has a typo in the docstring. The docstring says to use "server add volume", but the command is actually "server volume add". This change fixes the typo in the docstring. Task: 46781 Story: 2010401 Change-Id: Ie19a24ead100dd9177669653a7a9997772ef4538 --- openstackclient/volume/v3/volume_attachment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/volume/v3/volume_attachment.py b/openstackclient/volume/v3/volume_attachment.py index c740183791..57a6da7342 100644 --- a/openstackclient/volume/v3/volume_attachment.py +++ b/openstackclient/volume/v3/volume_attachment.py @@ -82,7 +82,7 @@ class CreateVolumeAttachment(command.ShowOne): the volume to the server at the hypervisor level. As a result, it should typically only be used for troubleshooting issues with an existing server in combination with other tooling. For all other use cases, the 'server - volume add' command should be preferred. + add volume' command should be preferred. """ def get_parser(self, prog_name): From bd0727c4f897289722ba639930c9e979cfee534a Mon Sep 17 00:00:00 2001 From: whoami-rajat Date: Thu, 17 Nov 2022 18:35:01 +0530 Subject: [PATCH 059/595] Add option to create volume from backup Support for creating a volume from backup was added in microversio 3.47. This patch adds a --backup option to the volume create command to add that support. Change-Id: Ib26d2d335475d9aacbf77c0fd7b7cda2ba743943 --- .../tests/unit/volume/v2/test_volume.py | 73 +++++++++++++++++++ openstackclient/volume/v2/volume.py | 29 +++++++- ...option-to-create-vol-fc36c2c745ebcff5.yaml | 4 + 3 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/add-backup-option-to-create-vol-fc36c2c745ebcff5.yaml diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index f802f637f7..ef9c2fab5a 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -16,6 +16,7 @@ from unittest import mock from unittest.mock import call +from cinderclient import api_versions from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib import utils @@ -47,6 +48,9 @@ def setUp(self): self.snapshots_mock = self.app.client_manager.volume.volume_snapshots self.snapshots_mock.reset_mock() + self.backups_mock = self.app.client_manager.volume.backups + self.backups_mock.reset_mock() + self.types_mock = self.app.client_manager.volume.volume_types self.types_mock.reset_mock() @@ -129,6 +133,7 @@ def test_volume_create_min_options(self): source_volid=None, consistencygroup_id=None, scheduler_hints=None, + backup_id=None, ) self.assertEqual(self.columns, columns) @@ -174,6 +179,7 @@ def test_volume_create_options(self): source_volid=None, consistencygroup_id=consistency_group.id, scheduler_hints={'k': 'v'}, + backup_id=None, ) self.assertEqual(self.columns, columns) @@ -210,6 +216,7 @@ def test_volume_create_properties(self): source_volid=None, consistencygroup_id=None, scheduler_hints=None, + backup_id=None, ) self.assertEqual(self.columns, columns) @@ -248,6 +255,7 @@ def test_volume_create_image_id(self): source_volid=None, consistencygroup_id=None, scheduler_hints=None, + backup_id=None, ) self.assertEqual(self.columns, columns) @@ -286,6 +294,7 @@ def test_volume_create_image_name(self): source_volid=None, consistencygroup_id=None, scheduler_hints=None, + backup_id=None, ) self.assertEqual(self.columns, columns) @@ -323,11 +332,72 @@ def test_volume_create_with_snapshot(self): source_volid=None, consistencygroup_id=None, scheduler_hints=None, + backup_id=None, + ) + + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.datalist, data) + + def test_volume_create_with_backup(self): + backup = volume_fakes.create_one_backup() + self.new_volume.backup_id = backup.id + arglist = [ + '--backup', self.new_volume.backup_id, + self.new_volume.name, + ] + verifylist = [ + ('backup', self.new_volume.backup_id), + ('name', self.new_volume.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.backups_mock.get.return_value = backup + + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.47') + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + self.volumes_mock.create.assert_called_once_with( + size=backup.size, + snapshot_id=None, + name=self.new_volume.name, + description=None, + volume_type=None, + availability_zone=None, + metadata=None, + imageRef=None, + source_volid=None, + consistencygroup_id=None, + scheduler_hints=None, + backup_id=backup.id, ) self.assertEqual(self.columns, columns) self.assertCountEqual(self.datalist, data) + def test_volume_create_with_backup_pre_347(self): + backup = volume_fakes.create_one_backup() + self.new_volume.backup_id = backup.id + arglist = [ + '--backup', self.new_volume.backup_id, + self.new_volume.name, + ] + verifylist = [ + ('backup', self.new_volume.backup_id), + ('name', self.new_volume.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.backups_mock.get.return_value = backup + + exc = self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + self.assertIn("--os-volume-api-version 3.47 or greater", str(exc)) + def test_volume_create_with_bootable_and_readonly(self): arglist = [ '--bootable', @@ -361,6 +431,7 @@ def test_volume_create_with_bootable_and_readonly(self): source_volid=None, consistencygroup_id=None, scheduler_hints=None, + backup_id=None, ) self.assertEqual(self.columns, columns) @@ -403,6 +474,7 @@ def test_volume_create_with_nonbootable_and_readwrite(self): source_volid=None, consistencygroup_id=None, scheduler_hints=None, + backup_id=None, ) self.assertEqual(self.columns, columns) @@ -454,6 +526,7 @@ def test_volume_create_with_bootable_and_readonly_fail( source_volid=None, consistencygroup_id=None, scheduler_hints=None, + backup_id=None, ) self.assertEqual(2, mock_error.call_count) diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index 1e1fde9226..53f6e643d3 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -71,10 +71,10 @@ def _check_size_arg(args): volume is not specified. """ - if ((args.snapshot or args.source) + if ((args.snapshot or args.source or args.backup) is None and args.size is None): - msg = _("--size is a required option if snapshot " - "or source volume is not specified.") + msg = _("--size is a required option if snapshot, backup " + "or source volume are not specified.") raise exceptions.CommandError(msg) @@ -117,6 +117,12 @@ def get_parser(self, prog_name): metavar="", help=_("Volume to clone (name or ID)"), ) + source_group.add_argument( + "--backup", + metavar="", + help=_("Restore backup to a volume (name or ID) " + "(supported by --os-volume-api-version 3.47 or later)"), + ) source_group.add_argument( "--source-replicated", metavar="", @@ -177,9 +183,16 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): _check_size_arg(parsed_args) + volume_client = self.app.client_manager.volume image_client = self.app.client_manager.image + if parsed_args.backup and not ( + volume_client.api_version.matches('3.47')): + msg = _("--os-volume-api-version 3.47 or greater is required " + "to create a volume from backup.") + raise exceptions.CommandError(msg) + source_volume = None if parsed_args.source: source_volume = utils.find_resource( @@ -213,6 +226,15 @@ def take_action(self, parsed_args): # snapshot size. size = max(size or 0, snapshot_obj.size) + backup = None + if parsed_args.backup: + backup_obj = utils.find_resource( + volume_client.backups, + parsed_args.backup) + backup = backup_obj.id + # As above + size = max(size or 0, backup_obj.size) + volume = volume_client.volumes.create( size=size, snapshot_id=snapshot, @@ -225,6 +247,7 @@ def take_action(self, parsed_args): source_volid=source_volume, consistencygroup_id=consistency_group, scheduler_hints=parsed_args.hint, + backup_id=backup, ) if parsed_args.bootable or parsed_args.non_bootable: diff --git a/releasenotes/notes/add-backup-option-to-create-vol-fc36c2c745ebcff5.yaml b/releasenotes/notes/add-backup-option-to-create-vol-fc36c2c745ebcff5.yaml new file mode 100644 index 0000000000..081ebcea74 --- /dev/null +++ b/releasenotes/notes/add-backup-option-to-create-vol-fc36c2c745ebcff5.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added ``--backup`` option to the ``volume create`` command. From 96162c24eaba5ba70a6d8f61da815d2d6ffea7ed Mon Sep 17 00:00:00 2001 From: whoami-rajat Date: Tue, 22 Nov 2022 20:59:13 +0530 Subject: [PATCH 060/595] Change --size helptext to include backup Followup from [1]. Modifying help text of --size argument to include --backup option. [1] https://review.opendev.org/c/openstack/python-openstackclient/+/864893 Change-Id: I12cf60079ebcfe1cd059602fbfc1a13c8fe86803 --- openstackclient/volume/v2/volume.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index 53f6e643d3..ffcbd573fb 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -93,8 +93,8 @@ def get_parser(self, prog_name): "--size", metavar="", type=int, - help=_("Volume size in GB (Required unless --snapshot or " - "--source is specified)"), + help=_("Volume size in GB (required unless --snapshot, " + "--source or --backup is specified)"), ) parser.add_argument( "--type", From 4710cbeca6638cc78880f2c4fc22d74aa7bb7c41 Mon Sep 17 00:00:00 2001 From: whoami-rajat Date: Tue, 22 Nov 2022 19:25:39 +0530 Subject: [PATCH 061/595] Add test for creating volume from source This patch adds a test to create a new volume from source. We also include code changes to pass the right size i.e. either size passed by the user via --size argument or the source volume size. This case is already handled at the API layer[1] but it helps being consistent with passing the right size value as in case of creating a volume from snapshot or backup. [1] https://github.com/openstack/cinder/blob/7c1a5ce7b11964da4537fd6a7d157ede646b9e94/cinder/api/v3/volumes.py#L381-L382 Change-Id: Idc71636dad6bb678fe24f19b0836d2e9bd92d7d2 --- .../tests/unit/volume/v2/test_volume.py | 37 +++++++++++++++++++ openstackclient/volume/v2/volume.py | 13 +++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index ef9c2fab5a..c930002f3d 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -398,6 +398,43 @@ def test_volume_create_with_backup_pre_347(self): parsed_args) self.assertIn("--os-volume-api-version 3.47 or greater", str(exc)) + def test_volume_create_with_source_volume(self): + source_vol = "source_vol" + arglist = [ + '--source', self.new_volume.id, + source_vol, + ] + verifylist = [ + ('source', self.new_volume.id), + ('name', source_vol), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.volumes_mock.get.return_value = self.new_volume + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + self.volumes_mock.create.assert_called_once_with( + size=self.new_volume.size, + snapshot_id=None, + name=source_vol, + description=None, + volume_type=None, + availability_zone=None, + metadata=None, + imageRef=None, + source_volid=self.new_volume.id, + consistencygroup_id=None, + scheduler_hints=None, + backup_id=None, + ) + + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.datalist, data) + def test_volume_create_with_bootable_and_readonly(self): arglist = [ '--bootable', diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index ffcbd573fb..7905e0971c 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -183,6 +183,11 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): _check_size_arg(parsed_args) + # size is validated in the above call to + # _check_size_arg where we check that size + # should be passed if we are not creating a + # volume from snapshot, backup or source volume + size = parsed_args.size volume_client = self.app.client_manager.volume image_client = self.app.client_manager.image @@ -195,9 +200,11 @@ def take_action(self, parsed_args): source_volume = None if parsed_args.source: - source_volume = utils.find_resource( + source_volume_obj = utils.find_resource( volume_client.volumes, - parsed_args.source).id + parsed_args.source) + source_volume = source_volume_obj.id + size = max(size or 0, source_volume_obj.size) consistency_group = None if parsed_args.consistency_group: @@ -210,8 +217,6 @@ def take_action(self, parsed_args): image = image_client.find_image(parsed_args.image, ignore_missing=False).id - size = parsed_args.size - snapshot = None if parsed_args.snapshot: snapshot_obj = utils.find_resource( From abf1a7cc4b1caada799aa4549f9753b76146e243 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 24 Nov 2022 12:59:33 +0000 Subject: [PATCH 062/595] docs: Small cleanup of human interface guide Before we add more content. Change-Id: I6cf28bdd217326db991466a21221b685124d4b99 Signed-off-by: Stephen Finucane --- .../contributor/humaninterfaceguide.rst | 80 ++++++++++--------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/doc/source/contributor/humaninterfaceguide.rst b/doc/source/contributor/humaninterfaceguide.rst index a7db380053..29fba40e05 100644 --- a/doc/source/contributor/humaninterfaceguide.rst +++ b/doc/source/contributor/humaninterfaceguide.rst @@ -117,14 +117,14 @@ interface to the user, not the user to the interface. Commands should be discoverable via the interface itself. -To determine a list of available commands, use the :code:`-h` or -:code:`--help` options: +To determine a list of available commands, use the ``-h`` or +``--help`` options: .. code-block:: bash $ openstack --help -For help with an individual command, use the :code:`help` command: +For help with an individual command, use the ``help`` command: .. code-block:: bash @@ -167,7 +167,7 @@ Command Structure OpenStackClient has a consistent and predictable format for all of its commands. -* The top level command name is :code:`openstack` +* The top level command name is ``openstack`` * Sub-commands take the form: .. code-block:: bash @@ -193,50 +193,56 @@ invocation regardless of action to be performed. They include authentication credentials and API version selection. Most global options have a corresponding environment variable that may also be used to set the value. If both are present, the command-line option takes priority. The environment variable names are derived -from the option name by dropping the leading dashes ('--'), converting each embedded -dash ('-') to an underscore ('_'), and converting to upper case. +from the option name by dropping the leading dashes (``--``), converting each embedded +dash (``-``) to an underscore (``_``), and converting to upper case. * Global options shall always have a long option name, certain common options may also have short names. Short names should be reserved for global options to limit the potential for duplication and multiple meanings between commands given the limited set of available short names. -* All long options names shall begin with two dashes ('--') and use a single dash - ('-') internally between words (:code:`--like-this`). Underscores ('_') shall not + +* All long options names shall begin with two dashes (``--``) and use a single dash + (``-`` internally between words (``--like-this``). Underscores (``_``) shall not be used in option names. + * Authentication options conform to the common CLI authentication guidelines in :ref:`authentication`. -For example, :code:`--os-username` can be set from the environment via -:code:`OS_USERNAME`. +For example, ``--os-username`` can be set from the environment via +``OS_USERNAME``. ---help -++++++ +``--help`` +++++++++++ -The standard :code:`--help` global option displays the documentation for invoking +The standard ``--help`` global option displays the documentation for invoking the program and a list of the available commands on standard output. All other options and commands are ignored when this is present. The traditional short -form help option (:code:`-h`) is also available. +form help option (``-h``) is also available. ---version -+++++++++ +``--version`` ++++++++++++++ -The standard :code:`--version` option displays the name and version on standard +The standard ``--version`` option displays the name and version on standard output. All other options and commands are ignored when this is present. Command Object(s) and Action ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Commands consist of an object described by one or more words followed by an action. Commands that require two objects have the primary object ahead of the action and the secondary object after the action. Any positional arguments identifying the objects shall appear in the same order as the objects. In badly formed English it is expressed as "(Take) object1 (and perform) action (using) object2 (to it)." +Commands consist of an object described by one or more words followed by an +action. Commands that require two objects have the primary object ahead of the +action and the secondary object after the action. Any positional arguments +identifying the objects shall appear in the same order as the objects. In +badly formed English it is expressed as "(Take) object-1 (and perform) action +(using) object-2 (to it).":: [] Examples: -* :code:`group add user ` -* :code:`volume type list` # Note that :code:`volume type` is a two-word - single object +* ``group add user `` +* ``volume type list`` (note that ``volume type`` is a two-word single object) -The :code:`help` command is unique as it appears in front of a normal command +The ``help`` command is unique as it appears in front of a normal command and displays the help text for that command rather than execute it. Object names are always specified in command in their singular form. This is @@ -256,21 +262,21 @@ meaning across multiple commands. Option Forms ++++++++++++ -* **boolean**: boolean options shall use a form of :code:`--|--` - (preferred) or :code:`--